Browse Source

Fix/performance (#782)

* Performance improvements in the UI.

* Fix styles in schema-category.
pull/785/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
88b8ebc13c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      frontend/app/features/content/pages/calendar/calendar-page.component.ts
  2. 38
      frontend/app/features/content/pages/content/editor/content-field.component.ts
  3. 6
      frontend/app/features/content/shared/content-extension.component.ts
  4. 5
      frontend/app/features/content/shared/forms/array-item.component.ts
  5. 58
      frontend/app/features/content/shared/forms/field-editor.component.html
  6. 25
      frontend/app/features/content/shared/forms/field-editor.component.ts
  7. 6
      frontend/app/features/content/shared/forms/stock-photo-editor.component.ts
  8. 9
      frontend/app/features/schemas/pages/schema/fields/types/number-ui.component.ts
  9. 9
      frontend/app/features/schemas/pages/schema/fields/types/string-ui.component.ts
  10. 55
      frontend/app/framework/angular/forms/control-errors.component.ts
  11. 50
      frontend/app/framework/angular/forms/forms-helper.spec.ts
  12. 60
      frontend/app/framework/angular/forms/forms-helper.ts
  13. 25
      frontend/app/framework/angular/stop-drag.directive.ts
  14. 1
      frontend/app/framework/declarations.ts
  15. 4
      frontend/app/framework/module.ts
  16. 12
      frontend/app/shared/components/schema-category.component.html
  17. 24
      frontend/app/shared/components/schema-category.component.scss
  18. 4
      frontend/app/shared/state/contents.forms.ts
  19. 6
      frontend/app/shared/state/languages.forms.ts
  20. 6
      frontend/app/theme/_common.scss
  21. 4
      frontend/app/theme/_mixins.scss
  22. 4
      frontend/app/theme/_panels2.scss

4
frontend/app/features/content/pages/calendar/calendar-page.component.ts

@ -60,10 +60,10 @@ export class CalendarPageComponent implements AfterViewInit, OnDestroy {
const Calendar = tui.Calendar;
this.calendar = new Calendar(this.calendarContainer.nativeElement, {
taskView: false,
scheduleView: ['time'],
defaultView: 'month',
isReadOnly: true,
scheduleView: ['time'],
taskView: false,
...getLocalizationSettings(),
});

38
frontend/app/features/content/pages/content/editor/content-field.component.ts

@ -6,9 +6,8 @@
*/
import { Component, EventEmitter, HostBinding, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { AppLanguageDto, AppsState, EditContentForm, FieldForm, invalid$, LocalStoreService, SchemaDto, Settings, TranslationsService, Types, value$ } from '@app/shared';
import { combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { AppLanguageDto, AppsState, changed$, EditContentForm, FieldForm, invalid$, LocalStoreService, SchemaDto, Settings, TranslationsService } from '@app/shared';
import { Observable } from 'rxjs';
@Component({
selector: 'sqx-content-field[form][formContext][formModel][language][languages][schema]',
@ -79,11 +78,7 @@ export class ContentFieldComponent implements OnChanges {
}
if ((changes['formModel'] || changes['formModelCompare']) && this.formModelCompare) {
this.isDifferent =
combineLatest([
value$(this.formModel.form),
value$(this.formModelCompare!.form),
]).pipe(map(([lhs, rhs]) => !Types.equals(lhs, rhs, true)));
this.isDifferent = changed$(this.formModel.form, this.formModelCompare.form);
}
}
@ -110,31 +105,36 @@ export class ContentFieldComponent implements OnChanges {
public translate() {
const master = this.languages.find(x => x.isMaster);
if (master) {
if (!master) {
return;
}
const masterCode = master.iso2Code;
const masterValue = this.formModel.get(masterCode)!.form.value;
if (masterValue) {
if (!masterValue) {
return;
}
if (this.showAllControls) {
for (const language of this.languages) {
if (!language.isMaster) {
for (const language of this.languages.filter(x => !x.isMaster)) {
this.translateValue(masterValue, masterCode, language.iso2Code);
}
}
} else {
this.translateValue(masterValue, masterCode, this.language.iso2Code);
}
}
}
}
private translateValue(text: string, sourceLanguage: string, targetLanguage: string) {
const control = this.formModel.get(targetLanguage);
if (control) {
const value = control.form.value;
if (!control) {
return;
}
if (control.form.value) {
return;
}
if (!value) {
const request = { text, sourceLanguage, targetLanguage };
this.translations.translate(this.appsState.appName, request)
@ -144,8 +144,6 @@ export class ContentFieldComponent implements OnChanges {
}
});
}
}
}
public prefix(language: AppLanguageDto) {
return `(${language.iso2Code})`;

6
frontend/app/features/content/shared/content-extension.component.ts

@ -20,15 +20,15 @@ export class ContentExtensionComponent extends ResourceOwner implements OnChange
private readonly context: any;
private isInitialized = false;
@ViewChild('iframe', { static: false })
public iframe: ElementRef<HTMLIFrameElement>;
@Input()
public content?: ContentDto | null;
@Input()
public contentSchema: SchemaDto;
@ViewChild('iframe', { static: false })
public iframe: ElementRef<HTMLIFrameElement>;
@Input()
public set url(value: string | undefined | null) {
this.computedUrl = computeEditorUrl(value, this.appsState.snapshot.selectedSettings);

5
frontend/app/features/content/shared/forms/array-item.component.ts

@ -6,9 +6,8 @@
*/
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnInit, Output, QueryList, SimpleChanges, ViewChildren } from '@angular/core';
import { AppLanguageDto, ComponentForm, EditContentForm, FieldDto, FieldFormatter, FieldSection, invalid$, ObjectForm, RootFieldDto, StatefulComponent, Types, value$ } from '@app/shared';
import { AppLanguageDto, ComponentForm, EditContentForm, FieldDto, FieldFormatter, FieldSection, invalid$, ObjectForm, RootFieldDto, StatefulComponent, Types, valueProjection$ } from '@app/shared';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ComponentSectionComponent } from './component-section.component';
interface State {
@ -88,7 +87,7 @@ export class ArrayItemComponent extends StatefulComponent<State> implements OnCh
if (changes['formModel']) {
this.isInvalid = invalid$(this.formModel.form);
this.title = value$(this.formModel.form).pipe(map(x => this.getTitle(x)));
this.title = valueProjection$(this.formModel.form, x => this.getTitle(x));
}
}

58
frontend/app/features/content/shared/forms/field-editor.component.html

@ -5,13 +5,13 @@
<small class="field-disabled ps-1" *ngIf="field.isDisabled">Disabled</small>
<sqx-control-errors *ngIf="form" [for]="editorControl" [fieldName]="field.displayName"></sqx-control-errors>
<sqx-control-errors *ngIf="form" [for]="$any(fieldForm)" [fieldName]="field.displayName"></sqx-control-errors>
<div>
<ng-container *ngIf="field.properties.editorUrl; else noEditor">
<sqx-iframe-editor [url]="field.properties.editorUrl" #editor
[context]="formContext"
[formControl]="editorControl"
[formControl]="$any(fieldForm)"
[formValue]="form.valueChanges | async"
[formIndex]="index"
[language]="language?.iso2Code">
@ -31,16 +31,16 @@
</sqx-array-editor>
</ng-container>
<ng-container *ngSwitchCase="'Assets'">
<sqx-assets-editor [formControl]="editorControl" [folderId]="field.rawProperties.folderId"></sqx-assets-editor>
<sqx-assets-editor [formControl]="$any(fieldForm)" [folderId]="field.rawProperties.folderId"></sqx-assets-editor>
</ng-container>
<ng-container *ngSwitchCase="'Boolean'">
<ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Toggle'">
<sqx-toggle [formControl]="editorControl" [threeStates]="!field.properties.isRequired"></sqx-toggle>
<sqx-toggle [formControl]="$any(fieldForm)" [threeStates]="!field.properties.isRequired"></sqx-toggle>
</ng-container>
<ng-container *ngSwitchCase="'Checkbox'">
<div class="form-check">
<input class="form-check-input" type="checkbox" [formControl]="editorControl" id="{{uniqueId}}" sqxIndeterminateValue [threeStates]="!field.properties.isRequired">
<input class="form-check-input" type="checkbox" [formControl]="$any(fieldForm)" id="{{uniqueId}}" sqxIndeterminateValue [threeStates]="!field.properties.isRequired">
<label class="form-check-label" for="{{uniqueId}}"></label>
</div>
</ng-container>
@ -67,31 +67,31 @@
</sqx-array-editor>
</ng-container>
<ng-container *ngSwitchCase="'DateTime'">
<sqx-date-time-editor [formControl]="editorControl" [mode]="field.rawProperties.editor" [enforceTime]="true"></sqx-date-time-editor>
<sqx-date-time-editor [formControl]="$any(fieldForm)" [mode]="field.rawProperties.editor" [enforceTime]="true"></sqx-date-time-editor>
</ng-container>
<ng-container *ngSwitchCase="'Geolocation'">
<sqx-geolocation-editor [formControl]="editorControl"></sqx-geolocation-editor>
<sqx-geolocation-editor [formControl]="$any(fieldForm)"></sqx-geolocation-editor>
</ng-container>
<ng-container *ngSwitchCase="'Json'">
<sqx-code-editor [formControl]="editorControl" valueMode="Json" [height]="350"></sqx-code-editor>
<sqx-code-editor [formControl]="$any(fieldForm)" valueMode="Json" [height]="350"></sqx-code-editor>
</ng-container>
<ng-container *ngSwitchCase="'Number'">
<ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Input'">
<input class="form-control" type="number" [formControl]="editorControl" [placeholder]="field.displayPlaceholder">
<input class="form-control" type="number" [formControl]="$any(fieldForm)" [placeholder]="field.displayPlaceholder">
</ng-container>
<ng-container *ngSwitchCase="'Stars'">
<sqx-stars [formControl]="editorControl" [maximumStars]="field.rawProperties.maxValue"></sqx-stars>
<sqx-stars [formControl]="$any(fieldForm)" [maximumStars]="field.rawProperties.maxValue"></sqx-stars>
</ng-container>
<ng-container *ngSwitchCase="'Dropdown'">
<select class="form-select" [formControl]="editorControl">
<select class="form-select" [formControl]="$any(fieldForm)">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.rawProperties.allowedValues" [ngValue]="value">{{value}}</option>
</select>
</ng-container>
<ng-container *ngSwitchCase="'Radio'">
<div class="form-check" *ngFor="let value of field.rawProperties.allowedValues">
<input class="form-check-input" type="radio" [value]="value" [formControl]="editorControl" [name]="uniqueId" id="{{uniqueId}}_{{value}}">
<input class="form-check-input" type="radio" [value]="value" [formControl]="$any(fieldForm)" [name]="uniqueId" id="{{uniqueId}}_{{value}}">
<label class="form-check-label" for="{{uniqueId}}_{{value}}">
{{value}}
</label>
@ -103,7 +103,7 @@
<ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'List'">
<sqx-references-editor
[formControl]="editorControl"
[formControl]="$any(fieldForm)"
[allowDuplicates]="field.rawProperties.allowDuplicated"
[formContext]="formContext"
[language]="language"
@ -114,21 +114,21 @@
<ng-container *ngSwitchCase="'Dropdown'">
<sqx-reference-dropdown
mode="Array"
[formControl]="editorControl"
[formControl]="$any(fieldForm)"
[language]="language"
[schemaId]="field.rawProperties.singleId">
</sqx-reference-dropdown>
</ng-container>
<ng-container *ngSwitchCase="'Tags'">
<sqx-references-tags
[formControl]="editorControl"
[formControl]="$any(fieldForm)"
[language]="language"
[schemaId]="field.rawProperties.singleId">
</sqx-references-tags>
</ng-container>
<ng-container *ngSwitchCase="'Checkboxes'">
<sqx-references-checkboxes
[formControl]="editorControl"
[formControl]="$any(fieldForm)"
[language]="language"
[schemaId]="field.rawProperties.singleId">
</sqx-references-checkboxes>
@ -138,55 +138,55 @@
<ng-container *ngSwitchCase="'String'">
<ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Input'">
<input class="form-control" type="text" [formControl]="editorControl" [placeholder]="field.displayPlaceholder">
<input class="form-control" type="text" [formControl]="$any(fieldForm)" [placeholder]="field.displayPlaceholder">
</ng-container>
<ng-container *ngSwitchCase="'Slug'">
<input class="form-control" type="text" [formControl]="editorControl" [placeholder]="field.displayPlaceholder" sqxTransformInput="Slugify">
<input class="form-control" type="text" [formControl]="$any(fieldForm)" [placeholder]="field.displayPlaceholder" sqxTransformInput="Slugify">
</ng-container>
<ng-container *ngSwitchCase="'TextArea'">
<textarea class="form-control" [formControl]="editorControl" [placeholder]="field.displayPlaceholder" rows="5"></textarea>
<textarea class="form-control" [formControl]="$any(fieldForm)" [placeholder]="field.displayPlaceholder" rows="5"></textarea>
</ng-container>
<ng-container *ngSwitchCase="'RichText'">
<sqx-rich-editor [formControl]="editorControl" #editor [folderId]="field.rawProperties.folderId"></sqx-rich-editor>
<sqx-rich-editor [formControl]="$any(fieldForm)" #editor [folderId]="field.rawProperties.folderId"></sqx-rich-editor>
</ng-container>
<ng-container *ngSwitchCase="'Html'">
<sqx-code-editor [formControl]="editorControl" #editor mode="ace/mode/html" [height]="350" ></sqx-code-editor>
<sqx-code-editor [formControl]="$any(fieldForm)" #editor mode="ace/mode/html" [height]="350" ></sqx-code-editor>
</ng-container>
<ng-container *ngSwitchCase="'Markdown'">
<sqx-markdown-editor [formControl]="editorControl" #editor [folderId]="field.rawProperties.folderId"></sqx-markdown-editor>
<sqx-markdown-editor [formControl]="$any(fieldForm)" #editor [folderId]="field.rawProperties.folderId"></sqx-markdown-editor>
</ng-container>
<ng-container *ngSwitchCase="'StockPhoto'">
<sqx-stock-photo-editor [formControl]="editorControl"></sqx-stock-photo-editor>
<sqx-stock-photo-editor [formControl]="$any(fieldForm)"></sqx-stock-photo-editor>
</ng-container>
<ng-container *ngSwitchCase="'Dropdown'">
<select class="form-select" [formControl]="editorControl">
<select class="form-select" [formControl]="$any(fieldForm)">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.rawProperties.allowedValues" [ngValue]="value">{{value}}</option>
</select>
</ng-container>
<ng-container *ngSwitchCase="'Radio'">
<div class="form-check custom-control-inline" *ngFor="let value of field.rawProperties.allowedValues">
<input class="form-check-input" type="radio" [value]="value" [formControl]="editorControl" [name]="uniqueId" id="{{uniqueId}}_{{value}}">
<input class="form-check-input" type="radio" [value]="value" [formControl]="$any(fieldForm)" [name]="uniqueId" id="{{uniqueId}}_{{value}}">
<label class="form-check-label" for="{{uniqueId}}_{{value}}">
{{value}}
</label>
</div>
</ng-container>
<ng-container *ngSwitchCase="'Color'">
<sqx-color-picker [formControl]="editorControl" [placeholder]="field.displayPlaceholder"></sqx-color-picker>
<sqx-color-picker [formControl]="$any(fieldForm)" [placeholder]="field.displayPlaceholder"></sqx-color-picker>
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'Tags'">
<ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Tags'">
<sqx-tag-editor [formControl]="editorControl" [placeholder]="field.displayPlaceholder" [suggestions]="field.rawProperties.allowedValues"></sqx-tag-editor>
<sqx-tag-editor [formControl]="$any(fieldForm)" [placeholder]="field.displayPlaceholder" [suggestions]="field.rawProperties.allowedValues"></sqx-tag-editor>
</ng-container>
<ng-container *ngSwitchCase="'Checkboxes'">
<sqx-checkbox-group [formControl]="editorControl" [values]="field.rawProperties.allowedValues"></sqx-checkbox-group>
<sqx-checkbox-group [formControl]="$any(fieldForm)" [values]="field.rawProperties.allowedValues"></sqx-checkbox-group>
</ng-container>
<ng-container *ngSwitchCase="'Dropdown'">
<select multiple class="form-select" [formControl]="editorControl">
<select multiple class="form-select" [formControl]="$any(fieldForm)">
<option *ngFor="let value of field.rawProperties.allowedValues" [ngValue]="value">{{value}}</option>
</select>
</ng-container>

25
frontend/app/features/content/shared/forms/field-editor.component.ts

@ -6,10 +6,9 @@
*/
import { Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
import { AbstractControl, FormControl } from '@angular/forms';
import { AbstractContentForm, AppLanguageDto, EditContentForm, FieldDto, MathHelper, RootFieldDto, Types, value$ } from '@app/shared';
import { AbstractControl } from '@angular/forms';
import { AbstractContentForm, AppLanguageDto, EditContentForm, FieldDto, hasNoValue$, MathHelper, Types } from '@app/shared';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({
selector: 'sqx-field-editor[form][formContext][formModel][language][languages]',
@ -17,6 +16,8 @@ import { map } from 'rxjs/operators';
templateUrl: './field-editor.component.html',
})
export class FieldEditorComponent implements OnChanges {
public readonly uniqueId = MathHelper.guid();
@Input()
public form: EditContentForm;
@ -50,25 +51,13 @@ export class FieldEditorComponent implements OnChanges {
return this.formModel.field;
}
public get editorControl() {
return this.formModel.form as FormControl;
}
public get rootField() {
return this.formModel.field as RootFieldDto;
public get fieldForm() {
return this.formModel.form;
}
public uniqueId = MathHelper.guid();
public ngOnChanges(changes: SimpleChanges) {
if (changes['formModel']) {
const previousControl: AbstractContentForm<FieldDto, AbstractControl> = changes['formModel'].previousValue;
if (previousControl && Types.isFunction(previousControl.form['_clearChangeFns'])) {
previousControl.form['_clearChangeFns']();
}
this.isEmpty = value$(this.formModel.form).pipe(map(x => Types.isUndefined(x) || Types.isNull(x)));
this.isEmpty = hasNoValue$(this.formModel.form);
}
}

6
frontend/app/features/content/shared/forms/stock-photo-editor.component.ts

@ -7,9 +7,9 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core';
import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { StatefulControlComponent, StockPhotoDto, StockPhotoService, thumbnail, Types, value$ } from '@app/shared';
import { StatefulControlComponent, StockPhotoDto, StockPhotoService, thumbnail, Types, value$, valueProjection$ } from '@app/shared';
import { of } from 'rxjs';
import { debounceTime, map, switchMap, tap } from 'rxjs/operators';
import { debounceTime, switchMap, tap } from 'rxjs/operators';
export const SQX_STOCK_PHOTO_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StockPhotoEditorComponent), multi: true,
@ -40,7 +40,7 @@ export class StockPhotoEditorComponent extends StatefulControlComponent<State, s
public valueControl = new FormControl('');
public stockPhotoThumbnail = value$(this.valueControl).pipe(map(v => thumbnail(v, 400) || v));
public stockPhotoThumbnail = valueProjection$(this.valueControl, x => thumbnail(x, 400) || x);
public stockPhotoSearch = new FormControl('');
public stockPhotos =

9
frontend/app/features/schemas/pages/schema/fields/types/number-ui.component.ts

@ -7,9 +7,8 @@
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { FieldDto, FloatConverter, NumberFieldPropertiesDto, NUMBER_FIELD_EDITORS, ResourceOwner, value$ } from '@app/shared';
import { FieldDto, FloatConverter, NumberFieldPropertiesDto, NUMBER_FIELD_EDITORS, ResourceOwner, valueProjection$ } from '@app/shared';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({
selector: 'sqx-number-ui[field][fieldForm][properties]',
@ -36,11 +35,13 @@ export class NumberUIComponent extends ResourceOwner implements OnChanges {
if (changes['fieldForm']) {
this.unsubscribeAll();
const editor = this.fieldForm.controls['editor'];
this.hideAllowedValues =
value$<string>(this.fieldForm.controls['editor']).pipe(map(x => !(x && (x === 'Radio' || x === 'Dropdown'))));
valueProjection$(editor, x => !(x && (x === 'Radio' || x === 'Dropdown')));
this.hideInlineEditable =
value$<string>(this.fieldForm.controls['editor']).pipe(map(x => x === 'Radio'));
valueProjection$(editor, x => x === 'Radio');
this.own(
this.hideAllowedValues.subscribe(isSelection => {

9
frontend/app/features/schemas/pages/schema/fields/types/string-ui.component.ts

@ -7,9 +7,8 @@
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { FieldDto, ResourceOwner, StringFieldPropertiesDto, STRING_FIELD_EDITORS, value$ } from '@app/shared';
import { FieldDto, ResourceOwner, StringFieldPropertiesDto, STRING_FIELD_EDITORS, valueProjection$ } from '@app/shared';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({
selector: 'sqx-string-ui[field][fieldForm][properties]',
@ -35,11 +34,13 @@ export class StringUIComponent extends ResourceOwner implements OnChanges {
if (changes['fieldForm']) {
this.unsubscribeAll();
const editor = this.fieldForm.controls['editor'];
this.hideAllowedValues =
value$<string>(this.fieldForm.controls['editor']).pipe(map(x => !(x && (x === 'Radio' || x === 'Dropdown'))));
valueProjection$(editor, x => !(x && (x === 'Radio' || x === 'Dropdown')));
this.hideInlineEditable =
value$<string>(this.fieldForm.controls['editor']).pipe(map(x => !(x && (x === 'Input' || x === 'Dropdown' || x === 'Slug'))));
valueProjection$(editor, x => !(x && (x === 'Input' || x === 'Dropdown' || x === 'Slug')));
this.own(
this.hideAllowedValues.subscribe(isSelection => {

55
frontend/app/framework/angular/forms/control-errors.component.ts

@ -9,6 +9,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Host, Input, OnC
import { AbstractControl, FormArray, FormGroupDirective } from '@angular/forms';
import { fadeAnimation, LocalizerService, StatefulComponent, Types } from '@app/framework/internal';
import { merge } from 'rxjs';
import { touchedChange$ } from './forms-helper';
import { formatError } from './error-formatting';
interface State {
@ -27,8 +28,7 @@ interface State {
})
export class ControlErrorsComponent extends StatefulComponent<State> implements OnChanges, OnDestroy {
private displayFieldName: string;
private control: AbstractControl;
private controlOriginalMarkAsTouched: any;
private control: AbstractControl | null = null;
@Input()
public for: string | AbstractControl;
@ -37,7 +37,7 @@ export class ControlErrorsComponent extends StatefulComponent<State> implements
public fieldName: string | null | undefined;
public get isTouched() {
return this.control.touched || Types.is(this.control, FormArray);
return this.control?.touched || Types.is(this.control, FormArray);
}
constructor(changeDetector: ChangeDetectorRef,
@ -49,13 +49,9 @@ export class ControlErrorsComponent extends StatefulComponent<State> implements
});
}
public ngOnDestroy() {
super.ngOnDestroy();
this.unsetCustomMarkAsTouchedFunction();
}
public ngOnChanges() {
const previousControl = this.control;
if (this.fieldName) {
this.displayFieldName = this.fieldName;
} else if (this.for) {
@ -72,53 +68,34 @@ export class ControlErrorsComponent extends StatefulComponent<State> implements
}
}
let control: AbstractControl | null = null;
if (Types.isString(this.for)) {
if (this.formGroupDirective && this.formGroupDirective.form) {
control = this.formGroupDirective.form.controls[this.for];
this.control = this.formGroupDirective.form.controls[this.for];
} else {
this.control = null;
}
} else {
control = this.for;
this.control = this.for;
}
if (this.control !== control && control) {
if (this.control !== previousControl) {
this.unsubscribeAll();
this.unsetCustomMarkAsTouchedFunction();
this.control = control;
if (control) {
if (this.control) {
this.own(
merge(control.valueChanges, control.statusChanges)
.subscribe(() => {
merge(
this.control.valueChanges,
this.control.statusChanges,
touchedChange$(this.control),
).subscribe(() => {
this.createMessages();
}));
this.controlOriginalMarkAsTouched = this.control.markAsTouched;
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
// eslint-disable-next-line func-names
this.control['markAsTouched'] = function () {
// eslint-disable-next-line prefer-rest-params
self.controlOriginalMarkAsTouched.apply(this, arguments);
self.createMessages();
};
}
}
this.createMessages();
}
private unsetCustomMarkAsTouchedFunction() {
if (this.control && this.controlOriginalMarkAsTouched) {
this.control['markAsTouched'] = this.controlOriginalMarkAsTouched;
}
}
private createMessages() {
const errorMessages: string[] = [];

50
frontend/app/framework/angular/forms/forms-helper.spec.ts

@ -6,7 +6,7 @@
*/
import { FormArray, FormControl, FormGroup, Validators } from '@angular/forms';
import { getControlPath, value$ } from './forms-helper';
import { getControlPath, hasNoValue$, hasValue$, touchedChange$, value$ } from './forms-helper';
describe('FormHelpers', () => {
describe('value$', () => {
@ -44,6 +44,54 @@ describe('FormHelpers', () => {
});
});
it('should provide touched changes', () => {
const form = new FormControl('1', Validators.required);
const values: any[] = [];
touchedChange$(form).subscribe(x => {
values.push(x);
});
form.markAsTouched();
form.markAsUntouched();
form.markAsTouched();
expect(values).toEqual([false, true, false, true]);
});
it('should provide value when defined', () => {
const form = new FormControl('1', Validators.required);
const values: any[] = [];
hasValue$(form).subscribe(x => {
values.push(x);
});
form.setValue(undefined);
form.setValue('1');
form.setValue(null);
expect(values).toEqual([true, false, true, false]);
});
it('should provide value when defined', () => {
const form = new FormControl('1', Validators.required);
const values: any[] = [];
hasNoValue$(form).subscribe(x => {
values.push(x);
});
form.setValue(undefined);
form.setValue('1');
form.setValue(null);
expect(values).toEqual([false, true, false, true]);
});
describe('getControlPath', () => {
it('should calculate path for standalone control', () => {
const control = new FormControl();

60
frontend/app/framework/angular/forms/forms-helper.ts

@ -6,7 +6,7 @@
*/
import { AbstractControl, FormArray, FormGroup, ValidatorFn } from '@angular/forms';
import { Observable } from 'rxjs';
import { combineLatest, Observable } from 'rxjs';
import { distinctUntilChanged, map, startWith } from 'rxjs/operators';
import { Types } from './../../utils/types';
@ -96,19 +96,67 @@ export function invalid$(form: AbstractControl): Observable<boolean> {
}
export function value$<T = any>(form: AbstractControl): Observable<T> {
return form.valueChanges.pipe(startWith(form.value), distinctUntilChanged());
return form.valueChanges.pipe(map(() => getRawValue(form)), startWith(getRawValue(form)), distinctUntilChanged());
}
export function valueAll$<T = any>(form: AbstractControl): Observable<T> {
return form.valueChanges.pipe(map(() => getRawValue(form)), startWith(getRawValue(form)), distinctUntilChanged());
export function valueProjection$<T = any>(form: AbstractControl, projection: (value: any) => T): Observable<T> {
return value$(form).pipe(map(projection), distinctUntilChanged());
}
export function hasValue$(form: AbstractControl): Observable<boolean> {
return value$(form).pipe(map(v => !!v));
return valueProjection$(form, v => isValid(v));
}
export function hasNoValue$(form: AbstractControl): Observable<boolean> {
return value$(form).pipe(map(v => !v));
return valueProjection$(form, v => !isValid(v));
}
export function changed$(lhs: AbstractControl, rhs: AbstractControl) {
return combineLatest([
value$(lhs),
value$(rhs),
]).pipe(map(([lhs, rhs]) => !Types.equals(lhs, rhs, true)),
distinctUntilChanged());
}
export function touchedChange$(form: AbstractControl) {
return new Observable(subscriber => {
let previousTouched = form.touched;
const updateTouched = (touched: boolean) => {
if (touched !== previousTouched) {
subscriber.next(touched);
previousTouched = touched;
}
};
subscriber.next(form.touched);
const previousMarkedAsTouched = form.markAsTouched;
const previousMarkedAsUntouched = form.markAsUntouched;
form['markAsTouched'] = function markAsTouched(...args: any[]) {
previousMarkedAsTouched.apply(this, args);
updateTouched(form.touched);
};
form['markAsUntouched'] = function markAsTouched(...args: any[]) {
previousMarkedAsUntouched.apply(this, args);
updateTouched(form.touched);
};
return () => {
form['markAsTouched'] = previousMarkedAsTouched;
form['markAsUntouched'] = previousMarkedAsUntouched;
};
});
}
function isValid(value: any) {
return !Types.isNull(value) && !Types.isUndefined(value);
}
export function getRawValue(form: AbstractControl): any {

25
frontend/app/framework/angular/stop-drag.directive.ts

@ -0,0 +1,25 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Directive, HostListener, Input } from '@angular/core';
@Directive({
selector: '[sqxStopDrag]',
})
export class StopDragDirective {
@Input('sqxStopDrag')
public shouldStop: any = true;
@HostListener('dragstart', ['$event'])
public onDragStart(event: Event) {
const shouldStop: any = this.shouldStop;
if (shouldStop || shouldStop === '') {
event.preventDefault();
}
}
}

1
frontend/app/framework/declarations.ts

@ -72,6 +72,7 @@ export * from './angular/shortcut.component';
export * from './angular/shortcut.directive';
export * from './angular/status-icon.component';
export * from './angular/stop-click.directive';
export * from './angular/stop-drag.directive';
export * from './angular/sync-scrolling.directive';
export * from './angular/sync-width.directive';
export * from './angular/tab-router-link.directive';

4
frontend/app/framework/module.ts

@ -11,7 +11,7 @@ import { ModuleWithProviders, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { ColorPickerModule } from 'ngx-color-picker';
import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterceptor, CanDeactivateGuard, CheckboxGroupComponent, ClipboardService, CodeComponent, CodeEditorComponent, ColorPickerComponent, ConfirmClickDirective, ControlErrorsComponent, CopyDirective, DarkenPipe, DatePipe, DateTimeEditorComponent, DayOfWeekPipe, DayPipe, DialogRendererComponent, DialogService, DisplayNamePipe, DropdownComponent, DurationPipe, EditableTitleComponent, ExternalLinkDirective, FileDropDirective, FileSizePipe, FocusOnInitDirective, FormAlertComponent, FormErrorComponent, FormHintComponent, FromNowPipe, FullDateTimePipe, HighlightPipe, HoverBackgroundDirective, ImageSourceDirective, IndeterminateValueDirective, ISODatePipe, KeysPipe, KNumberPipe, LanguageSelectorComponent, LayoutComponent, LayoutContainerDirective, LightenPipe, ListViewComponent, LoadingInterceptor, LoadingService, LocalizedInputComponent, LocalStoreService, MarkdownInlinePipe, MarkdownPipe, MessageBus, ModalDialogComponent, ModalDirective, ModalPlacementDirective, MoneyPipe, MonthPipe, OnboardingService, OnboardingTooltipComponent, PagerComponent, ParentLinkDirective, PopupLinkDirective, ProgressBarComponent, ResizedDirective, ResizeService, ResourceLoaderService, RootViewComponent, SafeHtmlPipe, SafeResourceUrlPipe, SafeUrlPipe, ScrollActiveDirective, ShortcutComponent, ShortcutDirective, ShortcutService, ShortDatePipe, ShortTimePipe, StarsComponent, StatusIconComponent, StopClickDirective, SyncScollingDirective, SyncWidthDirective, TabRouterlinkDirective, TagEditorComponent, TemplateWrapperDirective, TempService, TitleComponent, TitleService, ToggleComponent, ToolbarComponent, TooltipDirective, TransformInputDirective, TranslatePipe, VideoPlayerComponent } from './declarations';
import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterceptor, CanDeactivateGuard, CheckboxGroupComponent, ClipboardService, CodeComponent, CodeEditorComponent, ColorPickerComponent, ConfirmClickDirective, ControlErrorsComponent, CopyDirective, DarkenPipe, DatePipe, DateTimeEditorComponent, DayOfWeekPipe, DayPipe, DialogRendererComponent, DialogService, DisplayNamePipe, DropdownComponent, DurationPipe, EditableTitleComponent, ExternalLinkDirective, FileDropDirective, FileSizePipe, FocusOnInitDirective, FormAlertComponent, FormErrorComponent, FormHintComponent, FromNowPipe, FullDateTimePipe, HighlightPipe, HoverBackgroundDirective, ImageSourceDirective, IndeterminateValueDirective, ISODatePipe, KeysPipe, KNumberPipe, LanguageSelectorComponent, LayoutComponent, LayoutContainerDirective, LightenPipe, ListViewComponent, LoadingInterceptor, LoadingService, LocalizedInputComponent, LocalStoreService, MarkdownInlinePipe, MarkdownPipe, MessageBus, ModalDialogComponent, ModalDirective, ModalPlacementDirective, MoneyPipe, MonthPipe, OnboardingService, OnboardingTooltipComponent, PagerComponent, ParentLinkDirective, PopupLinkDirective, ProgressBarComponent, ResizedDirective, ResizeService, ResourceLoaderService, RootViewComponent, SafeHtmlPipe, SafeResourceUrlPipe, SafeUrlPipe, ScrollActiveDirective, ShortcutComponent, ShortcutDirective, ShortcutService, ShortDatePipe, ShortTimePipe, StarsComponent, StatusIconComponent, StopClickDirective, StopDragDirective, SyncScollingDirective, SyncWidthDirective, TabRouterlinkDirective, TagEditorComponent, TemplateWrapperDirective, TempService, TitleComponent, TitleService, ToggleComponent, ToolbarComponent, TooltipDirective, TransformInputDirective, TranslatePipe, VideoPlayerComponent } from './declarations';
@NgModule({
imports: [
@ -88,6 +88,7 @@ import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterc
StarsComponent,
StatusIconComponent,
StopClickDirective,
StopDragDirective,
SyncScollingDirective,
SyncWidthDirective,
TabRouterlinkDirective,
@ -171,6 +172,7 @@ import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterc
StarsComponent,
StatusIconComponent,
StopClickDirective,
StopDragDirective,
SyncScollingDirective,
SyncWidthDirective,
TabRouterlinkDirective,

12
frontend/app/shared/components/schema-category.component.html

@ -1,4 +1,4 @@
<ul *ngIf="!forContent || schemas.length > 0" class="nav nav-light flex-column"
<ul *ngIf="!forContent || schemas.length > 0" class="nav nav-light flex-column droppable"
cdkDropList
cdkDropListSortingDisabled
[cdkDropListData]="schemaCategory.name"
@ -29,7 +29,7 @@
</div>
</li>
<div *ngIf="!isCollapsed" @fade [style.height]="getContainerHeight()">
<div *ngIf="!isCollapsed" @fade [style.height]="getContainerHeight()" class="nav-collapsed">
<ng-container *ngIf="!forContent; else simpleMode">
<li *ngFor="let schema of schemas; trackBy: trackBySchema" class="nav-item truncate" [style.height]="getItemHeight()"
cdkDrag
@ -37,7 +37,9 @@
[cdkDragData]="schema"
(cdkDragStarted)="dragStarted($event)">
<a class="nav-link truncate" [routerLink]="schemaRoute(schema)" routerLinkActive="active" title="{{schema.displayName}}" titlePosition="top-left">
<a class="nav-link truncate" [routerLink]="schemaRoute(schema)" routerLinkActive="active" sqxStopDrag
title="{{schema.displayName}}"
titlePosition="top-left">
<i cdkDragHandle class="icon-drag2 drag-handle"></i>
<span class="item-published me-1" [class.unpublished]="!schema.isPublished"></span> {{schema.displayName}}
@ -47,7 +49,9 @@
<ng-template #simpleMode>
<li *ngFor="let schema of schemas; trackBy: trackBySchema" class="nav-item truncate">
<a class="nav-link truncate" [routerLink]="schemaRoute(schema)" routerLinkActive="active" title="{{schema.displayName}}" titlePosition="top-left">
<a class="nav-link truncate drag-none" [routerLink]="schemaRoute(schema)" routerLinkActive="active" sqxStopDrag
title="{{schema.displayName}}"
titlePosition="top-left">
{{schema.displayName}}
</a>
</li>

24
frontend/app/shared/components/schema-category.component.scss

@ -16,17 +16,14 @@ $drag-margin: -8px;
}
}
.category {
margin-bottom: 1rem;
}
.droppable {
position: relative;
.drop-indicator {
@include absolute($drag-margin, $drag-margin, $drag-margin, $drag-margin);
background: none;
border: 2px dashed $color-black;
border: 2px dashed $color-border;
border-radius: 4px;
display: none;
pointer-events: none;
}
@ -58,6 +55,15 @@ $drag-margin: -8px;
.nav-heading {
margin-left: -1rem;
margin-top: 0;
}
.nav-collapsed {
max-width: 100%;
}
.nav-light {
margin-top: 1rem;
}
.nav-item {
@ -68,6 +74,14 @@ $drag-margin: -8px;
}
}
:host {
&:first-child {
.nav-light {
margin-top: 1rem;
}
}
}
.item-published {
margin-bottom: 1px;
}

4
frontend/app/shared/state/contents.forms.ts

@ -6,7 +6,7 @@
*/
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { debounceTimeSafe, Form, getRawValue, Types, UndefinableFormArray, UndefinableFormGroup, valueAll$ } from '@app/framework';
import { debounceTimeSafe, Form, getRawValue, Types, UndefinableFormArray, UndefinableFormGroup, value$ } from '@app/framework';
import { BehaviorSubject, distinctUntilChanged, Observable } from 'rxjs';
import { AppLanguageDto } from './../services/app-languages.service';
import { LanguageDto } from './../services/languages.service';
@ -124,7 +124,7 @@ export class EditContentForm extends Form<FormGroup, any> {
return new FieldSection<RootFieldDto, FieldForm>(separator, forms);
});
valueAll$(this.form).pipe(debounceTimeSafe(debounce), distinctUntilChanged(Types.equals)).subscribe(value => {
value$(this.form).pipe(debounceTimeSafe(debounce), distinctUntilChanged(Types.equals)).subscribe(value => {
this.valueChange$.next(value);
this.updateState(value);

6
frontend/app/shared/state/languages.forms.ts

@ -6,7 +6,7 @@
*/
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Form, valueAll$ } from '@app/framework';
import { Form, value$ } from '@app/framework';
import { AppLanguageDto, UpdateAppLanguageDto } from './../services/app-languages.service';
import { LanguageDto } from './../services/languages.service';
@ -17,14 +17,14 @@ export class EditLanguageForm extends Form<FormGroup, UpdateAppLanguageDto, AppL
isOptional: false,
}));
valueAll$(this.form.controls['isMaster'])
value$(this.form.controls['isMaster'])
.subscribe(value => {
if (value) {
this.form.controls['isOptional'].setValue(false);
}
});
valueAll$(this.form.controls['isOptional'])
value$(this.form.controls['isOptional'])
.subscribe(value => {
if (value) {
this.form.controls['isMaster'].setValue(false);

6
frontend/app/theme/_common.scss

@ -42,6 +42,12 @@ hr {
.drag-handle {
color: $color-text-decent;
cursor: move;
font-size: 1.1rem;
font-weight: normal;
}
.drag-none {
@include no-drag;
}
//

4
frontend/app/theme/_mixins.scss

@ -215,3 +215,7 @@
min-width: 0;
width: auto;
}
@mixin no-drag {
-webkit-user-drag: none;
}

4
frontend/app/theme/_panels2.scss

@ -267,6 +267,10 @@
margin-left: -.5rem;
margin-right: -.5rem;
.nav-item {
max-width: 100%;
}
.nav-link {
border: 0;
border-radius: $border-radius;

Loading…
Cancel
Save