Browse Source

Ui/sections (#541)

* Field sections.

* Just some minor things.

* Final fixes for sections.

* Another fix.

* Build fix.
pull/544/head
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
e14bd3db78
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaField.cs
  2. 3
      frontend/app/features/content/declarations.ts
  3. 5
      frontend/app/features/content/module.ts
  4. 52
      frontend/app/features/content/pages/content/content-field.component.html
  5. 80
      frontend/app/features/content/pages/content/content-field.component.ts
  6. 11
      frontend/app/features/content/pages/content/content-page.component.html
  7. 7
      frontend/app/features/content/pages/content/content-page.component.ts
  8. 30
      frontend/app/features/content/pages/content/content-section.component.html
  9. 23
      frontend/app/features/content/pages/content/content-section.component.scss
  10. 75
      frontend/app/features/content/pages/content/content-section.component.ts
  11. 5
      frontend/app/features/content/pages/schemas/schemas-page.component.html
  12. 12
      frontend/app/features/content/shared/forms/array-editor.component.ts
  13. 18
      frontend/app/features/content/shared/forms/array-item.component.html
  14. 86
      frontend/app/features/content/shared/forms/array-item.component.ts
  15. 18
      frontend/app/features/content/shared/forms/array-section.component.html
  16. 9
      frontend/app/features/content/shared/forms/array-section.component.scss
  17. 55
      frontend/app/features/content/shared/forms/array-section.component.ts
  18. 28
      frontend/app/features/content/shared/forms/field-editor.component.html
  19. 26
      frontend/app/features/content/shared/forms/field-editor.component.ts
  20. 45
      frontend/app/features/content/shared/group-fields.pipe.ts
  21. 12
      frontend/app/features/content/shared/references/content-creator.component.html
  22. 14
      frontend/app/shared/components/schema-category.component.html
  23. 59
      frontend/app/shared/components/schema-category.component.ts
  24. 4
      frontend/app/shared/services/schemas.service.ts
  25. 8
      frontend/app/shared/services/schemas.types.ts

3
backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaField.cs

@ -6,13 +6,12 @@
// ==========================================================================
using System.Collections.Generic;
using P = Squidex.Domain.Apps.Core.Partitioning;
namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{
public sealed class UpsertSchemaField : UpsertSchemaFieldBase
{
public string Partitioning { get; set; } = P.Invariant.Key;
public string Partitioning { get; set; }
public List<UpsertSchemaNestedField> Nested { get; set; }
}

3
frontend/app/features/content/declarations.ts

@ -10,6 +10,7 @@ export * from './pages/content/content-event.component';
export * from './pages/content/content-field.component';
export * from './pages/content/content-history-page.component';
export * from './pages/content/content-page.component';
export * from './pages/content/content-section.component';
export * from './pages/content/field-languages.component';
export * from './pages/contents/contents-filters-page.component';
export * from './pages/contents/contents-page.component';
@ -19,9 +20,11 @@ export * from './shared/content-status.component';
export * from './shared/due-time-selector.component';
export * from './shared/forms/array-editor.component';
export * from './shared/forms/array-item.component';
export * from './shared/forms/array-section.component';
export * from './shared/forms/assets-editor.component';
export * from './shared/forms/field-editor.component';
export * from './shared/forms/stock-photo-editor.component';
export * from './shared/group-fields.pipe';
export * from './shared/list/content-list-cell.directive';
export * from './shared/list/content-list-field.component';
export * from './shared/list/content-list-header.component';

5
frontend/app/features/content/module.ts

@ -10,7 +10,7 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CanDeactivateGuard, ContentMustExistGuard, LoadLanguagesGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SqxFrameworkModule, SqxSharedModule, UnsetContentGuard } from '@app/shared';
import { ArrayEditorComponent, ArrayItemComponent, AssetsEditorComponent, CommentsPageComponent, ContentComponent, ContentCreatorComponent, ContentEventComponent, ContentFieldComponent, ContentHistoryPageComponent, ContentListCellDirective, ContentListFieldComponent, ContentListHeaderComponent, ContentListWidthPipe, ContentPageComponent, ContentSelectorComponent, ContentSelectorItemComponent, ContentsFiltersPageComponent, ContentsPageComponent, ContentStatusComponent, ContentValueComponent, ContentValueEditorComponent, CustomViewEditorComponent, DueTimeSelectorComponent, FieldEditorComponent, FieldLanguagesComponent, PreviewButtonComponent, ReferenceItemComponent, ReferencesEditorComponent, SchemasPageComponent, StockPhotoEditorComponent } from './declarations';
import { ArrayEditorComponent, ArrayItemComponent, ArraySectionComponent, AssetsEditorComponent, CommentsPageComponent, ContentComponent, ContentCreatorComponent, ContentEventComponent, ContentFieldComponent, ContentHistoryPageComponent, ContentListCellDirective, ContentListFieldComponent, ContentListHeaderComponent, ContentListWidthPipe, ContentPageComponent, ContentSectionComponent, ContentSelectorComponent, ContentSelectorItemComponent, ContentsFiltersPageComponent, ContentsPageComponent, ContentStatusComponent, ContentValueComponent, ContentValueEditorComponent, CustomViewEditorComponent, DueTimeSelectorComponent, FieldEditorComponent, FieldLanguagesComponent, GroupFieldsPipe, PreviewButtonComponent, ReferenceItemComponent, ReferencesEditorComponent, SchemasPageComponent, StockPhotoEditorComponent } from './declarations';
const routes: Routes = [
{
@ -76,6 +76,7 @@ const routes: Routes = [
declarations: [
ArrayEditorComponent,
ArrayItemComponent,
ArraySectionComponent,
AssetsEditorComponent,
CommentsPageComponent,
ContentComponent,
@ -88,6 +89,7 @@ const routes: Routes = [
ContentListHeaderComponent,
ContentListWidthPipe,
ContentPageComponent,
ContentSectionComponent,
ContentSelectorComponent,
ContentSelectorItemComponent,
ContentsFiltersPageComponent,
@ -99,6 +101,7 @@ const routes: Routes = [
DueTimeSelectorComponent,
FieldEditorComponent,
FieldLanguagesComponent,
GroupFieldsPipe,
PreviewButtonComponent,
ReferenceItemComponent,
ReferencesEditorComponent,

52
frontend/app/features/content/pages/content/content-field.component.html

@ -1,15 +1,15 @@
<div class="row no-gutters" [class.compare]="fieldFormCompare">
<div [class.col-12]="!fieldFormCompare" [class.col-6]="fieldFormCompare">
<div class="table-items-row" [class.field-invalid]="isInvalid | async" *ngIf="field.properties.isContentField; else uiField">
<div class="table-items-row" [class.field-invalid]="isInvalid | async">
<div class="languages-buttons">
<button *ngIf="isTranslatable" type="button" class="btn btn-text-secondary btn-sm mr-1" (click)="translate()" title="Autotranslate from master language">
<button *ngIf="canTranslate" type="button" class="btn btn-text-secondary btn-sm mr-1" (click)="translate()" title="Autotranslate from master language">
<i class="icon-translate"></i>
</button>
<sqx-field-languages
[field]="field"
[language]="language"
(languageChange)="languageChange.emit($event)"
[language]="language"
[languages]="languages"
[showAllControls]="showAllControls"
(showAllControlsChange)="changeShowAllControls($event)">
@ -19,52 +19,41 @@
<ng-container *ngIf="showAllControls; else singleControl">
<div class="form-group" *ngFor="let language of languages; trackBy: trackByLanguage">
<sqx-field-editor
[displaySuffix]="prefix(language)"
[control]="fieldForm.controls[language.iso2Code]"
[field]="field"
[form]="form"
[formContext]="formContext"
[field]="field"
[language]="language"
[languages]="languages"
[control]="fieldForm.controls[language.iso2Code]">
[displaySuffix]="prefix(language)">
</sqx-field-editor>
</div>
</ng-container>
<ng-template #singleControl>
<sqx-field-editor
[control]="getControl()"
[field]="field"
[form]="form"
[formContext]="formContext"
[field]="field"
[language]="language"
[languages]="languages"
[control]="selectedFormControl">
[languages]="languages">
</sqx-field-editor>
</ng-template>
</div>
<ng-template #uiField>
<sqx-field-editor
[form]="form"
[formContext]="formContext"
[field]="field"
[language]="language"
[languages]="languages"
[control]="selectedFormControl">
</sqx-field-editor>
</ng-template>
</div>
<div class="col-6 col-right" *ngIf="fieldFormCompare">
<button type="button" class="btn btn-primary btn-sm field-copy" (click)="copy()" *ngIf="field.properties.isContentField && (isDifferent | async)">
<button type="button" class="btn btn-primary btn-sm field-copy" (click)="copy()" *ngIf="isDifferent | async">
<i class="icon-arrow_back"></i>
</button>
<div class="table-items-row" *ngIf="field.properties.isContentField; else uiField">
<div class="table-items-row">
<div class="languages-buttons">
<sqx-field-languages
[field]="field"
[language]="language"
(languageChange)="languageChange.emit($event)"
[language]="language"
[languages]="languages"
[showAllControls]="showAllControls"
(showAllControlsChange)="changeShowAllControls($event)">
@ -74,32 +63,23 @@
<ng-container *ngIf="showAllControls; else singleControlCompare">
<div class="form-group" *ngFor="let language of languages; trackBy: trackByLanguage">
<sqx-field-editor
[displaySuffix]="prefix(language)"
[control]="fieldFormCompare?.controls[language.iso2Code]"
[field]="field"
[language]="language"
[languages]="languages"
[control]="fieldFormCompare?.controls[language.iso2Code]">
[displaySuffix]="prefix(language)">
</sqx-field-editor>
</div>
</ng-container>
<ng-template #singleControlCompare>
<sqx-field-editor
[control]="getControlCompare()"
[field]="field"
[language]="language"
[languages]="languages"
[control]="selectedFormControlCompare">
[languages]="languages">
</sqx-field-editor>
</ng-template>
</div>
<ng-template #uiField>
<sqx-field-editor
[field]="field"
[language]="language"
[languages]="languages"
[control]="selectedFormControl">
</sqx-field-editor>
</ng-template>
</div>
</div>

80
frontend/app/features/content/pages/content/content-field.component.ts

@ -5,9 +5,9 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, DoCheck, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { AbstractControl, FormGroup } from '@angular/forms';
import { AppLanguageDto, AppsState, EditContentForm, fieldInvariant, invalid$, LocalStoreService, RootFieldDto, SchemaDto, TranslationsService, Types, value$ } from '@app/shared';
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { AppLanguageDto, AppsState, EditContentForm, fieldInvariant, invalid$, LocalStoreService, RootFieldDto, SchemaDto, StringFieldPropertiesDto, TranslationsService, Types, value$ } from '@app/shared';
import { Observable } from 'rxjs';
import { combineLatest } from 'rxjs/operators';
@ -16,7 +16,7 @@ import { combineLatest } from 'rxjs/operators';
styleUrls: ['./content-field.component.scss'],
templateUrl: './content-field.component.html'
})
export class ContentFieldComponent implements DoCheck, OnChanges {
export class ContentFieldComponent implements OnChanges {
@Output()
public languageChange = new EventEmitter<AppLanguageDto>();
@ -44,14 +44,24 @@ export class ContentFieldComponent implements DoCheck, OnChanges {
@Input()
public languages: ReadonlyArray<AppLanguageDto>;
public selectedFormControl: AbstractControl;
public selectedFormControlCompare?: AbstractControl;
public showAllControls = false;
public isDifferent: Observable<boolean>;
public isInvalid: Observable<boolean>;
public isTranslatable: boolean;
public get canTranslate() {
if (this.languages.length <= 1) {
return false;
}
if (!this.field.isLocalizable) {
return false;
}
const properties = this.field.properties;
return Types.is(properties, StringFieldPropertiesDto) && (properties.editor === 'Input' || properties.editor === 'TextArea');
}
constructor(
private readonly appsState: AppsState,
@ -61,18 +71,12 @@ export class ContentFieldComponent implements DoCheck, OnChanges {
}
public ngOnChanges(changes: SimpleChanges) {
if (changes['field']) {
this.showAllControls = this.localStore.getBoolean(this.configKey());
}
this.showAllControls = this.localStore.getBoolean(this.configKey());
if (changes['fieldForm'] && this.fieldForm) {
this.isInvalid = invalid$(this.fieldForm);
}
if (changes['fieldForm'] || changes['field'] || changes['languages']) {
this.isTranslatable = this.field.isTranslatable;
}
if ((changes['fieldForm'] || changes['fieldFormCompare']) && this.fieldFormCompare) {
this.isDifferent =
value$(this.fieldForm).pipe(
@ -81,32 +85,6 @@ export class ContentFieldComponent implements DoCheck, OnChanges {
}
}
public ngDoCheck() {
if (this.fieldForm) {
const control = this.findControl(this.fieldForm);
if (this.selectedFormControl !== control) {
if (this.selectedFormControl && Types.isFunction(this.selectedFormControl['_clearChangeFns'])) {
this.selectedFormControl['_clearChangeFns']();
}
this.selectedFormControl = control;
}
if (this.fieldFormCompare) {
const controlCompare = this.findControl(this.fieldFormCompare);
if (this.selectedFormControlCompare !== controlCompare) {
if (this.selectedFormControlCompare && Types.isFunction(this.selectedFormControlCompare['_clearChangeFns'])) {
this.selectedFormControlCompare['_clearChangeFns']();
}
this.selectedFormControlCompare = controlCompare;
}
}
}
}
public changeShowAllControls(showAllControls: boolean) {
this.showAllControls = showAllControls;
@ -114,11 +92,11 @@ export class ContentFieldComponent implements DoCheck, OnChanges {
}
public copy() {
if (this.selectedFormControlCompare && this.fieldFormCompare) {
if (this.fieldFormCompare && this.fieldFormCompare) {
if (this.showAllControls) {
this.fieldForm.setValue(this.fieldFormCompare.value);
} else {
this.selectedFormControl.setValue(this.selectedFormControlCompare.value);
this.getControl()!.setValue(this.getControlCompare()!.value);
}
}
}
@ -163,11 +141,11 @@ export class ContentFieldComponent implements DoCheck, OnChanges {
}
}
private findControl(form: FormGroup) {
private findControl(form?: FormGroup) {
if (this.field.isLocalizable) {
return form.controls[this.language.iso2Code];
return form?.controls[this.language.iso2Code];
} else {
return form.controls[fieldInvariant];
return form?.controls[fieldInvariant];
}
}
@ -175,11 +153,19 @@ export class ContentFieldComponent implements DoCheck, OnChanges {
return `(${language.iso2Code})`;
}
public getControl() {
return this.findControl(this.fieldForm);
}
public getControlCompare() {
return this.findControl(this.fieldFormCompare);
}
public trackByLanguage(index: number, language: AppLanguageDto) {
return language.iso2Code;
}
private configKey() {
return `squidex.schemas.${this.schema.id}.fields.${this.field.fieldId}.show-all`;
return `squidex.schemas.${this.schema?.id}.fields.${this.field?.fieldId}.show-all`;
}
}

11
frontend/app/features/content/pages/content/content-page.component.html

@ -81,16 +81,15 @@
</ng-container>
<div content>
<sqx-content-field *ngFor="let field of schema.fields; trackBy: trackByField"
<sqx-content-section *ngFor="let section of schema.fields | sqxGroupFields; trackBy: trackBySection"
[(language)]="language"
[field]="field"
[fieldForm]="contentForm.form.get(field.name)"
[fieldFormCompare]="contentFormCompare?.form.get(field.name)"
[form]="contentForm"
[formCompare]="contentFormCompare"
[formContext]="formContext"
[languages]="languages"
[schema]="schema">
</sqx-content-field>
[schema]="schema"
[section]="section">
</sqx-content-section>
</div>
</sqx-list-view>

7
frontend/app/features/content/pages/content/content-page.component.ts

@ -9,9 +9,10 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ApiUrlConfig, AppLanguageDto, AuthService, AutoSaveKey, AutoSaveService, CanComponentDeactivate, ContentDto, ContentsState, DialogService, EditContentForm, fadeAnimation, FieldDto, LanguagesState, ModalModel, ResourceOwner, SchemaDetailsDto, SchemasState, TempService, Version } from '@app/shared';
import { ApiUrlConfig, AppLanguageDto, AuthService, AutoSaveKey, AutoSaveService, CanComponentDeactivate, ContentDto, ContentsState, DialogService, EditContentForm, fadeAnimation, LanguagesState, ModalModel, ResourceOwner, RootFieldDto, SchemaDetailsDto, SchemasState, TempService, Version } from '@app/shared';
import { Observable, of } from 'rxjs';
import { debounceTime, filter, onErrorResumeNext, tap } from 'rxjs/operators';
import { FieldSection } from '../../shared/group-fields.pipe';
@Component({
selector: 'sqx-content-page',
@ -249,8 +250,8 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
}
}
public trackByField(field: FieldDto) {
return field.fieldId;
public trackBySection(index: number, section: FieldSection<RootFieldDto>) {
return section.separator?.fieldId;
}
}

30
frontend/app/features/content/pages/content/content-section.component.html

@ -0,0 +1,30 @@
<div class="header" *ngIf="section.separator; let separator">
<div class="row no-gutters">
<div class="col-auto">
<button type="button" class="btn btn-sm btn-text-secondary" (click)="toggle()">
<i [class.icon-caret-right]="isCollapsed" [class.icon-caret-down]="!isCollapsed"></i>
</button>
</div>
<div class="col">
<h3>{{separator!.displayName}}</h3>
<sqx-form-hint *ngIf="separator!.properties.hints?.length > 0">
{{separator!.properties.hints}}
</sqx-form-hint>
</div>
</div>
</div>
<div [class.hidden]="isCollapsed">
<sqx-content-field *ngFor="let field of section.fields; trackBy: trackByField"
[field]="field"
[fieldForm]="getFieldForm(field)"
[fieldFormCompare]="getFieldFormCompare(field)"
[form]="form"
[formContext]="formContext"
(languageChange)="languageChange.emit($event)"
[language]="language"
[languages]="languages"
[schema]="schema">
</sqx-content-field>
</div>

23
frontend/app/features/content/pages/content/content-section.component.scss

@ -0,0 +1,23 @@
.btn {
& {
width: 2rem;
}
&:focus {
border-color: transparent;
}
}
h3 {
margin: 0;
}
.header {
line-height: 2rem;
margin-bottom: .5rem;
margin-top: 1.5rem;
h3 {
line-height: 2rem;
}
}

75
frontend/app/features/content/pages/content/content-section.component.ts

@ -0,0 +1,75 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { AppLanguageDto, EditContentForm, LocalStoreService, RootFieldDto, SchemaDto } from '@app/shared';
import { FieldSection } from './../../shared/group-fields.pipe';
@Component({
selector: 'sqx-content-section',
styleUrls: ['./content-section.component.scss'],
templateUrl: './content-section.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ContentSectionComponent implements OnChanges {
@Output()
public languageChange = new EventEmitter<AppLanguageDto>();
@Input()
public form: EditContentForm;
@Input()
public formCompare?: EditContentForm;
@Input()
public formContext: any;
@Input()
public schema: SchemaDto;
@Input()
public section: FieldSection<RootFieldDto>;
@Input()
public language: AppLanguageDto;
@Input()
public languages: ReadonlyArray<AppLanguageDto>;
public isCollapsed: boolean;
constructor(
private readonly localStore: LocalStoreService
) {
}
public ngOnChanges() {
this.isCollapsed = this.localStore.getBoolean(this.configKey());
}
public toggle() {
this.isCollapsed = !this.isCollapsed;
this.localStore.setBoolean(this.configKey(), this.isCollapsed);
}
public getFieldForm(field: RootFieldDto) {
return this.form.form.get(field.name)!;
}
public getFieldFormCompare(field: RootFieldDto) {
return this.formCompare?.form.get(field.name)!;
}
public trackByField(index: number, field: RootFieldDto) {
return field.fieldId;
}
private configKey(): string {
return `squidex.schemas.${this.schema?.id}.fields.${this.section?.separator?.fieldId}.closed`;
}
}

5
frontend/app/features/content/pages/schemas/schemas-page.component.html

@ -9,10 +9,7 @@
<ng-container header>
<a class="panel-close btn-collapse" [class.collapsed]="isCollapsed" (click)="toggle()">
<i
[class.icon-angle-double-left]="!isCollapsed"
[class.icon-angle-double-right]="isCollapsed">
</i>
<i [class.icon-angle-double-left]="!isCollapsed" [class.icon-angle-double-right]="isCollapsed"></i>
</a>
</ng-container>

12
frontend/app/features/content/shared/forms/array-editor.component.ts

@ -52,20 +52,20 @@ export class ArrayEditorComponent {
}
public collapseAll() {
this.children.forEach(component => {
component.collapse();
this.children.forEach(child => {
child.collapse();
});
}
public expandAll() {
this.children.forEach(component => {
component.expand();
this.children.forEach(child => {
child.expand();
});
}
private reset() {
this.children.forEach(component => {
component.reset();
this.children.forEach(child => {
child.reset();
});
}

18
frontend/app/features/content/shared/forms/array-item.component.html

@ -7,7 +7,7 @@
<div class="col">
<div class="truncate">
<span class="header-index">#{{index + 1}}</span>
<span class="header-title">{{title}}</span>
<span class="header-title">{{title | async }}</span>
</div>
</div>
<div class="col-auto pr-4">
@ -23,10 +23,10 @@
<button type="button" class="btn btn-text-secondary" [disabled]="isDisabled || isLast" (click)="moveBottom()" title="Move this item to bottom">
<i class="icon-caret-bottom"></i>
</button>
<button type="button" class="btn btn-text-secondary" [class.hidden]="!isHidden" (click)="expand()" title="Expand this item">
<button type="button" class="btn btn-text-secondary" [class.hidden]="!isCollapsed" (click)="expand()" title="Expand this item">
<i class="icon-plus-square"></i>
</button>
<button type="button" class="btn btn-text-secondary" [class.hidden]="isHidden" (click)="collapse()" title="Collapse this item">
<button type="button" class="btn btn-text-secondary" [class.hidden]="isCollapsed" (click)="collapse()" title="Collapse this item">
<i class="icon-minus-square"></i>
</button>
</div>
@ -42,16 +42,16 @@
</div>
</div>
<div class="card-body" [class.hidden]="isHidden">
<div class="form-group" *ngFor="let fieldControl of fieldControls; trackBy: trackByField">
<sqx-field-editor
<div class="card-body" [class.hidden]="isCollapsed">
<div class="form-group" *ngFor="let section of field.nested | sqxGroupFields; trackBy: trackBySection">
<sqx-array-section
[form]="form"
[formContext]="formContext"
[field]="fieldControl.field"
[itemForm]="itemForm"
[language]="language"
[languages]="languages"
[control]="fieldControl.control">
</sqx-field-editor>
[section]="section">
</sqx-array-section>
</div>
</div>
</div>

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

@ -5,23 +5,22 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnDestroy, Output, QueryList, SimpleChanges, ViewChildren } from '@angular/core';
import { AbstractControl, FormGroup } from '@angular/forms';
import { AppLanguageDto, EditContentForm, FieldDto, FieldFormatter, invalid$, RootFieldDto, value$ } from '@app/shared';
import { Observable, Subscription } from 'rxjs';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, QueryList, SimpleChanges, ViewChildren } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { AppLanguageDto, EditContentForm, FieldFormatter, invalid$, NestedFieldDto, RootFieldDto, value$ } from '@app/shared';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { FieldSection } from './../group-fields.pipe';
import { ArraySectionComponent } from './array-section.component';
import { FieldEditorComponent } from './field-editor.component';
type FieldControl = { field: FieldDto, control: AbstractControl };
@Component({
selector: 'sqx-array-item',
styleUrls: ['./array-item.component.scss'],
templateUrl: './array-item.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ArrayItemComponent implements OnChanges, OnDestroy {
private subscription: Subscription;
export class ArrayItemComponent implements OnChanges {
@Output()
public remove = new EventEmitter();
@ -62,85 +61,54 @@ export class ArrayItemComponent implements OnChanges, OnDestroy {
public languages: ReadonlyArray<AppLanguageDto>;
@ViewChildren(FieldEditorComponent)
public editors: QueryList<FieldEditorComponent>;
public sections: QueryList<ArraySectionComponent>;
public isHidden = false;
public isCollapsed = false;
public isInvalid: Observable<boolean>;
public title: string;
public fieldControls: ReadonlyArray<FieldControl> = [];
public title: Observable<string>;
constructor(
private readonly changeDetector: ChangeDetectorRef
) {
}
public ngOnDestroy() {
this.unsubscribeFromForm();
}
private unsubscribeFromForm() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
public ngOnChanges(changes: SimpleChanges) {
if (changes['itemForm']) {
this.isInvalid = invalid$(this.itemForm);
this.unsubscribeFromForm();
this.subscription =
value$(this.itemForm)
.subscribe(() => {
this.updateTitle();
});
}
if (changes['itemForm'] || changes['field']) {
this.updateFields();
this.updateTitle();
this.title = value$(this.itemForm).pipe(map(x => this.getTitle(x)));
}
}
private updateFields() {
const fields: FieldControl[] = [];
private getTitle(value: any) {
const values: string[] = [];
for (const field of this.field.nested) {
const control = this.itemForm.get(field.name)!;
if (control || this.field.properties.isContentField) {
fields.push({ field, control });
}
}
this.fieldControls = fields;
}
private updateTitle() {
const values: string[] = [];
const control = this.itemForm.get(field.name);
for (const { control, field } of this.fieldControls) {
const formatted = FieldFormatter.format(field, control.value);
if (control) {
const formatted = FieldFormatter.format(field, control.value);
if (formatted) {
values.push(formatted);
if (formatted) {
values.push(formatted);
}
}
}
this.title = values.join(', ');
return values.join(', ');
}
public collapse() {
this.isHidden = true;
this.isCollapsed = true;
this.changeDetector.markForCheck();
}
public expand() {
this.isHidden = false;
this.isCollapsed = false;
this.changeDetector.markForCheck();
}
@ -162,12 +130,12 @@ export class ArrayItemComponent implements OnChanges, OnDestroy {
}
public reset() {
this.editors.forEach(editor => {
editor.reset();
this.sections.forEach(section => {
section.reset();
});
}
public trackByField(index: number, control: FieldControl) {
return control.field.name;
public trackBySection(index: number, section: FieldSection<NestedFieldDto>) {
return section.separator?.fieldId;
}
}

18
frontend/app/features/content/shared/forms/array-section.component.html

@ -0,0 +1,18 @@
<div class="header" *ngIf="section.separator; let separator">
<h3>{{separator!.displayName}}</h3>
<sqx-form-hint *ngIf="separator!.properties.hints?.length > 0">
{{separator!.properties.hints}}
</sqx-form-hint>
</div>
<div class="form-group" *ngFor="let field of section.fields; trackBy: trackByField">
<sqx-field-editor
[control]="getControl(field)"
[field]="field"
[form]="form"
[formContext]="formContext"
[language]="language"
[languages]="languages">
</sqx-field-editor>
</div>

9
frontend/app/features/content/shared/forms/array-section.component.scss

@ -0,0 +1,9 @@
.header {
line-height: 2rem;
margin-bottom: .5rem;
margin-top: 1.5rem;
h3 {
line-height: 2rem;
}
}

55
frontend/app/features/content/shared/forms/array-section.component.ts

@ -0,0 +1,55 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, Input, QueryList, ViewChildren } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { AppLanguageDto, EditContentForm, NestedFieldDto } from '@app/shared';
import { FieldSection } from './../group-fields.pipe';
import { FieldEditorComponent } from './field-editor.component';
@Component({
selector: 'sqx-array-section',
styleUrls: ['./array-section.component.scss'],
templateUrl: './array-section.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ArraySectionComponent {
@Input()
public itemForm: FormGroup;
@Input()
public form: EditContentForm;
@Input()
public formContext: any;
@Input()
public language: AppLanguageDto;
@Input()
public languages: ReadonlyArray<AppLanguageDto>;
@Input()
public section: FieldSection<NestedFieldDto>;
@ViewChildren(FieldEditorComponent)
public editors: QueryList<FieldEditorComponent>;
public getControl(field: NestedFieldDto) {
return this.itemForm.get(field.name)!;
}
public reset() {
this.editors.forEach(editor => {
editor.reset();
});
}
public trackByField(index: number, field: NestedFieldDto) {
return field.fieldId;
}
}

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

@ -1,13 +1,11 @@
<div [class.ui]="!field.properties.isContentField" *ngIf="field">
<ng-container *ngIf="field.properties.isContentField">
<label>
{{field.displayName}} {{displaySuffix}} <span class="field-required" [class.hidden]="!field.properties.isRequired">*</span>
</label>
<small class="field-disabled pl-1" *ngIf="field.isDisabled">Disabled</small>
<sqx-control-errors *ngIf="form" [for]="editorControl" [fieldName]="field.displayName" [submitted]="form.submitted | async"></sqx-control-errors>
</ng-container>
<div *ngIf="field">
<label>
{{field.displayName}} {{displaySuffix}} <span class="field-required" [class.hidden]="!field.properties.isRequired">*</span>
</label>
<small class="field-disabled pl-1" *ngIf="field.isDisabled">Disabled</small>
<sqx-control-errors *ngIf="form" [for]="editorControl" [fieldName]="field.displayName" [submitted]="form.submitted | async"></sqx-control-errors>
<div>
<ng-container *ngIf="field.properties.editorUrl; else noEditor">
@ -172,11 +170,9 @@
</ng-container>
</ng-container>
</ng-template>
</div>
</div>
<ng-container *ngIf="field.properties.hints; let hints">
<sqx-form-hint *ngIf="hints.length > 0">
{{hints}}
</sqx-form-hint>
</ng-container>
<sqx-form-hint *ngIf="field.properties.hints?.length > 0">
{{field.properties.hints}}
</sqx-form-hint>
</div>

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

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, ElementRef, Input, ViewChild } from '@angular/core';
import { Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
import { AbstractControl, FormArray, FormControl } from '@angular/forms';
import { AppLanguageDto, EditContentForm, FieldDto, MathHelper, RootFieldDto, Types } from '@app/shared';
@ -14,7 +14,7 @@ import { AppLanguageDto, EditContentForm, FieldDto, MathHelper, RootFieldDto, Ty
styleUrls: ['./field-editor.component.scss'],
templateUrl: './field-editor.component.html'
})
export class FieldEditorComponent {
export class FieldEditorComponent implements OnChanges {
@Input()
public form: EditContentForm;
@ -53,13 +53,25 @@ export class FieldEditorComponent {
public uniqueId = MathHelper.guid();
public reset() {
if (this.editor.nativeElement && Types.isFunction(this.editor.nativeElement['reset'])) {
this.editor.nativeElement['reset']();
public ngOnChanges(changes: SimpleChanges) {
const previousControl = changes['control']?.previousValue;
if (previousControl && Types.isFunction(previousControl['_clearChangeFns'])) {
previousControl['_clearChangeFns']();
}
}
public reset() {
if (this.editor) {
const nativeElement = this.editor.nativeElement;
if (nativeElement && Types.isFunction(nativeElement['reset'])) {
nativeElement['reset']();
}
if (this.editor && Types.isFunction(this.editor['reset'])) {
this.editor['reset']();
if (this.editor && Types.isFunction(this.editor['reset'])) {
this.editor['reset']();
}
}
}
}

45
frontend/app/features/content/shared/group-fields.pipe.ts

@ -0,0 +1,45 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Pipe, PipeTransform } from '@angular/core';
import { FieldDto } from '@app/shared';
export interface FieldSection<T> {
separator?: T;
fields: ReadonlyArray<T>;
}
@Pipe({
name: 'sqxGroupFields',
pure: true
})
export class GroupFieldsPipe<T extends FieldDto> implements PipeTransform {
public transform(fields: ReadonlyArray<T>) {
const sections: FieldSection<T>[] = [];
let currentSeparator: T | undefined = undefined;
let currentFields: T[] = [];
for (const field of fields) {
if (field.properties.isContentField) {
currentFields.push(field);
} else {
sections.push({ separator: currentSeparator, fields: currentFields });
currentFields = [];
currentSeparator = field;
}
}
if (currentFields.length > 0) {
sections.push({ separator: currentSeparator, fields: currentFields });
}
return sections;
}
}

12
frontend/app/features/content/shared/references/content-creator.component.html

@ -40,16 +40,14 @@
<ng-container content>
<ng-container *ngIf="schema && contentForm">
<form [formGroup]="contentForm.form" (ngSubmit)="saveAndPublish()">
<sqx-content-field *ngFor="let field of schema.fields"
(languageChange)="selectLanguage($event)"
[field]="field"
[fieldForm]="contentForm.form.get(field.name)"
<sqx-content-section *ngFor="let section of schema.fields | sqxGroupFields"
[(language)]="language"
[form]="contentForm"
[formContext]="contentFormContext"
[language]="language"
[languages]="languages"
[schema]="schema">
</sqx-content-field>
[schema]="schema"
[section]="section">
</sqx-content-section>
</form>
</ng-container>
</ng-container>

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

@ -1,4 +1,4 @@
<div *ngIf="!forContent || snapshot.filtered.length > 0" class="droppable category"
<div *ngIf="!forContent || filteredSchemas.length > 0" class="droppable category"
cdkDropList
cdkDropListSortingDisabled
[cdkDropListData]="schemaCategory.name"
@ -8,7 +8,7 @@
<div class="row no-gutters">
<div class="col-auto">
<button type="button" class="btn btn-sm btn-text-secondary" (click)="toggle()">
<i [class.icon-caret-right]="!snapshot.isOpen" [class.icon-caret-down]="snapshot.isOpen"></i>
<i [class.icon-caret-right]="isCollapsed" [class.icon-caret-down]="!isCollapsed"></i>
</button>
</div>
<div class="col">
@ -16,7 +16,7 @@
</div>
<div class="col-auto">
<ng-container *ngIf="schemaCategory.schemas.length > 0; else noSchemas">
({{snapshot.filtered.length}})
({{filteredSchemas.length}})
</ng-container>
<ng-template #noSchemas>
<button type="button" class="btn btn-sm btn-text-secondary btn-remove" (click)="remove.emit()">
@ -24,12 +24,12 @@
</button>
</ng-template>
</div>
</div>
</div>
</div>
<div class="nav nav-panel nav-dark nav-dark-bordered flex-column" *ngIf="snapshot.isOpen" @fade>
<div class="nav nav-panel nav-dark nav-dark-bordered flex-column" *ngIf="!isCollapsed" @fade>
<ng-container *ngIf="!forContent; else simpleMode">
<div *ngFor="let schema of snapshot.filtered; trackBy: trackBySchema" class="nav-item"
<div *ngFor="let schema of filteredSchemas; trackBy: trackBySchema" class="nav-item"
routerLinkActive="active"
cdkDropList
cdkDrag
@ -59,7 +59,7 @@
</ng-container>
<ng-template #simpleMode>
<li *ngFor="let schema of snapshot.filtered; trackBy: trackBySchema" class="nav-item">
<li *ngFor="let schema of filteredSchemas; trackBy: trackBySchema" class="nav-item">
<a class="nav-link" [routerLink]="schemaRoute(schema)" routerLinkActive="active">
<span class="schema-name" *ngIf="forContent">{{schema.displayName}}</span>
</a>

59
frontend/app/shared/components/schema-category.component.ts

@ -6,16 +6,8 @@
*/
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
import { fadeAnimation, LocalStoreService, SchemaCategory, SchemaDto, SchemasState, StatefulComponent } from '@app/shared/internal';
interface State {
// The filtered schemas.
filtered: ReadonlyArray<SchemaDto>;
// True when the category is open.
isOpen?: boolean;
}
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { fadeAnimation, LocalStoreService, SchemaCategory, SchemaDto, SchemasList, SchemasState } from '@app/shared/internal';
@Component({
selector: 'sqx-schema-category',
@ -26,7 +18,7 @@ interface State {
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SchemaCategoryComponent extends StatefulComponent<State> implements OnInit, OnChanges {
export class SchemaCategoryComponent implements OnChanges {
@Output()
public remove = new EventEmitter();
@ -39,42 +31,35 @@ export class SchemaCategoryComponent extends StatefulComponent<State> implements
@Input()
public forContent: boolean;
constructor(changeDetector: ChangeDetectorRef,
public filteredSchemas: SchemasList;
public isCollapsed = false;
constructor(
private readonly localStore: LocalStoreService,
private readonly schemasState: SchemasState
) {
super(changeDetector, { filtered: [], isOpen: true });
}
public ngOnInit() {
this.next(s => ({ ...s, isOpen: !this.localStore.getBoolean(this.configKey()) }));
}
public toggle() {
this.next(s => ({ ...s, isOpen: !s.isOpen }));
this.isCollapsed = !this.isCollapsed;
this.localStore.setBoolean(this.configKey(), !this.snapshot.isOpen);
this.localStore.setBoolean(this.configKey(), this.isCollapsed);
}
public ngOnChanges(changes: SimpleChanges): void {
if (changes['schemaCategory'] || changes['schemasFilter']) {
let filtered = this.schemaCategory.schemas;
if (this.forContent) {
filtered = filtered.filter(x => x.canReadContents && x.isPublished);
}
let isOpen = false;
public ngOnChanges() {
this.filteredSchemas = this.schemaCategory.schemas;
if (this.schemasFilter) {
filtered = filtered.filter(x => x.name.indexOf(this.schemasFilter) >= 0);
if (this.forContent) {
this.filteredSchemas = this.filteredSchemas.filter(x => x.canReadContents && x.isPublished);
}
isOpen = true;
} else {
isOpen = this.localStore.get(`schema-category.${this.schemaCategory.name}`) !== 'false';
}
if (this.schemasFilter) {
this.filteredSchemas = this.filteredSchemas.filter(x => x.name.indexOf(this.schemasFilter) >= 0);
this.next(s => ({ ...s, isOpen, filtered }));
this.isCollapsed = false;
} else {
this.isCollapsed = this.localStore.getBoolean(this.configKey());
}
}
@ -92,11 +77,11 @@ export class SchemaCategoryComponent extends StatefulComponent<State> implements
}
}
public trackBySchema(index: number, schema: SchemaDto) {
public trackBySchema(schema: SchemaDto) {
return schema.id;
}
private configKey(): string {
return `squidex.schema.category.${this.schemaCategory.name}.closed`;
return `squidex.schema.category.${this.schemaCategory.name}.collapsed`;
}
}

4
frontend/app/shared/services/schemas.service.ts

@ -285,10 +285,6 @@ export class RootFieldDto extends FieldDto {
return this.properties.fieldType === 'Array';
}
public get isTranslatable() {
return this.isLocalizable && this.properties.isTranslateable;
}
constructor(links: ResourceLinks, fieldId: number, name: string, properties: FieldPropertiesDto,
public readonly partitioning: string,
isLocked: boolean = false,

8
frontend/app/shared/services/schemas.types.ts

@ -139,10 +139,6 @@ export abstract class FieldPropertiesDto {
public readonly placeholder?: string;
public readonly tags?: ReadonlyArray<string>;
public get isTranslateable() {
return false;
}
public get isComplexUI() {
return true;
}
@ -329,10 +325,6 @@ export class StringFieldPropertiesDto extends FieldPropertiesDto {
return this.editor !== 'Input' && this.editor !== 'Color' && this.editor !== 'Radio' && this.editor !== 'Slug' && this.editor !== 'TextArea';
}
public get isTranslateable() {
return this.editor === 'Input' || this.editor === 'TextArea';
}
public accept<T>(visitor: FieldPropertiesVisitor<T>): T {
return visitor.visitString(this);
}

Loading…
Cancel
Save