Browse Source

Collapsible fields in schema editor.

pull/942/head
Sebastian 4 years ago
parent
commit
3c7e7be062
  1. 2
      frontend/src/app/features/schemas/declarations.ts
  2. 6
      frontend/src/app/features/schemas/module.ts
  3. 45
      frontend/src/app/features/schemas/pages/schema/fields/field.component.html
  4. 61
      frontend/src/app/features/schemas/pages/schema/fields/field.component.scss
  5. 19
      frontend/src/app/features/schemas/pages/schema/fields/field.component.ts
  6. 28
      frontend/src/app/features/schemas/pages/schema/fields/schema-fields.component.html
  7. 19
      frontend/src/app/features/schemas/pages/schema/fields/schema-fields.component.ts
  8. 2
      frontend/src/app/framework/angular/drag-helper.ts
  9. 10
      frontend/src/app/shared/state/contents.forms-helpers.ts
  10. 2
      frontend/src/app/shared/state/schemas.state.ts
  11. 1
      frontend/src/app/shared/state/settings.ts

2
frontend/src/app/features/schemas/declarations.ts

@ -7,6 +7,7 @@
export * from './pages/schema/common/schema-edit-form.component';
export * from './pages/schema/export/schema-export-form.component';
export * from './pages/schema/fields/field-group.component';
export * from './pages/schema/fields/field-wizard.component';
export * from './pages/schema/fields/field.component';
export * from './pages/schema/fields/forms/field-form-common.component';
@ -14,6 +15,7 @@ export * from './pages/schema/fields/forms/field-form-ui.component';
export * from './pages/schema/fields/forms/field-form-validation.component';
export * from './pages/schema/fields/forms/field-form.component';
export * from './pages/schema/fields/schema-fields.component';
export * from './pages/schema/fields/sortable-field-list.component';
export * from './pages/schema/fields/types/array-validation.component';
export * from './pages/schema/fields/types/assets-ui.component';
export * from './pages/schema/fields/types/assets-validation.component';

6
frontend/src/app/features/schemas/module.ts

@ -8,9 +8,7 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HelpComponent, HistoryComponent, LoadSchemasGuard, SchemaMustExistGuard, SqxFrameworkModule, SqxSharedModule } from '@app/shared';
import { ArrayValidationComponent, AssetsUIComponent, AssetsValidationComponent, BooleanUIComponent, BooleanValidationComponent, ComponentsUIComponent, ComponentsValidationComponent, DateTimeUIComponent, DateTimeValidationComponent, FieldComponent, FieldFormCommonComponent, FieldFormComponent, FieldFormUIComponent, FieldFormValidationComponent, FieldListComponent, FieldWizardComponent, GeolocationUIComponent, GeolocationValidationComponent, JsonMoreComponent, JsonUIComponent, JsonValidationComponent, NumberUIComponent, NumberValidationComponent, ReferencesUIComponent, ReferencesValidationComponent, SchemaEditFormComponent, SchemaExportFormComponent, SchemaFieldRulesFormComponent, SchemaFieldsComponent, SchemaFormComponent, SchemaPageComponent, SchemaPreviewUrlsFormComponent, SchemaScriptNamePipe, SchemaScriptsFormComponent, SchemasPageComponent, SchemaUIFormComponent, StringUIComponent, StringValidationComponent, TagsUIComponent, TagsValidationComponent } from './declarations';
import { ComponentUIComponent } from './pages/schema/fields/types/component-ui.component';
import { ComponentValidationComponent } from './pages/schema/fields/types/component-validation.component';
import { ArrayValidationComponent, AssetsUIComponent, AssetsValidationComponent, BooleanUIComponent, BooleanValidationComponent, ComponentsUIComponent, ComponentsValidationComponent, ComponentUIComponent, ComponentValidationComponent, DateTimeUIComponent, DateTimeValidationComponent, FieldComponent, FieldFormCommonComponent, FieldFormComponent, FieldFormUIComponent, FieldFormValidationComponent, FieldGroupComponent, FieldListComponent, FieldWizardComponent, GeolocationUIComponent, GeolocationValidationComponent, JsonMoreComponent, JsonUIComponent, JsonValidationComponent, NumberUIComponent, NumberValidationComponent, ReferencesUIComponent, ReferencesValidationComponent, SchemaEditFormComponent, SchemaExportFormComponent, SchemaFieldRulesFormComponent, SchemaFieldsComponent, SchemaFormComponent, SchemaPageComponent, SchemaPreviewUrlsFormComponent, SchemaScriptNamePipe, SchemaScriptsFormComponent, SchemasPageComponent, SchemaUIFormComponent, SortableFieldListComponent, StringUIComponent, StringValidationComponent, TagsUIComponent, TagsValidationComponent } from './declarations';
const routes: Routes = [
{
@ -65,6 +63,7 @@ const routes: Routes = [
DateTimeUIComponent,
DateTimeValidationComponent,
FieldComponent,
FieldGroupComponent,
FieldFormCommonComponent,
FieldFormComponent,
FieldFormUIComponent,
@ -91,6 +90,7 @@ const routes: Routes = [
SchemaScriptNamePipe,
SchemasPageComponent,
SchemaUIFormComponent,
SortableFieldListComponent,
StringUIComponent,
StringValidationComponent,
TagsUIComponent,

45
frontend/src/app/features/schemas/pages/schema/fields/field.component.html

@ -1,13 +1,12 @@
<div class="table-items-row table-items-row-expandable field">
<div class="table-items-row table-items-row-expandable field" [class.plain]="plain">
<div class="table-items-row-summary">
<span class="drag-container">
<ng-content></ng-content>
</span>
<div class="row align-items-center">
<div class="col-6">
<div class="col col-auto">
<ng-content></ng-content>
</div>
<div class="col">
<span class="field-name">
<i class="field-icon icon-type-{{field.properties.fieldType}}"></i>
<i *ngIf="!plain" class="field-icon icon-type-{{field.properties.fieldType}}"></i>
<ng-container *ngIf="field.isHidden else visible">
<span class="field-hidden" title="i18n:schemas.field.hiddenMarker">{{field.displayName}}</span>
@ -20,7 +19,7 @@
<span class="field-partitioning ms-2" *ngIf="field['isLocalizable']">{{ 'schemas.field.localizableMarker' | sqxTranslate }}</span>
</span>
</div>
<div class="col col-tags flex-nowrap">
<div class="col-4 flex-nowrap" *ngIf="!plain">
<div class="float-end">
<span class="ms-1 badge rounded-pill badge-danger" *ngIf="field.isLocked">
{{ 'schemas.field.lockedMarker' | sqxTranslate }}
@ -110,35 +109,31 @@
<ng-container *ngIf="field['nested']; let nested">
<span class="nested-field-line-v"></span>
<div
cdkDropList
[cdkDropListDisabled]="!isEditable"
[cdkDropListData]="nested"
(cdkDropListDropped)="sortFields($event)">
<div *ngFor="let nested of nested; trackBy: trackByFieldFn" class="nested-field table-drag" cdkDrag cdkDragLockAxis="y">
<span class="nested-field-line-h"></span>
<sqx-field [field]="nested" [schema]="schema" [parent]="$any(field)" [settings]="settings" [languages]="languages">
<i cdkDragHandle class="icon-drag2 drag-handle"></i>
</sqx-field>
</div>
</div>
<sqx-sortable-field-list
[fields]="nested"
[fieldsEmpty]="nested.length === 0"
[languages]="languages"
[schema]="schema"
[settings]="settings"
[sortable]="isEditable === true"
(sorted)="sortFields($event)"
[parent]="$any(field)">
</sqx-sortable-field-list>
<div class="nested-field nested-field-add" *ngIf="isEditable">
<span class="nested-field-line-h"></span>
<button type="button" class="btn btn-success btn-sm" (click)="addFieldDialog.show()">
<button type="button" class="btn btn-success btn-sm" (click)="fieldWizard.show()">
<i class="icon icon-plus"></i> {{ 'schemas.addNestedField' | sqxTranslate }}
</button>
</div>
<ng-container *sqxModal="addFieldDialog">
<ng-container *sqxModal="fieldWizard">
<sqx-field-wizard
[parent]="$any(field)"
[schema]="schema"
[settings]="settings"
(complete)="addFieldDialog.hide()">
(complete)="fieldWizard.hide()">
</sqx-field-wizard>
</ng-container>
</ng-container>

61
frontend/src/app/features/schemas/pages/schema/fields/field.component.scss

@ -14,14 +14,23 @@ $padding: 1rem;
}
.table-items-row-summary {
padding-left: 3rem;
padding-right: 1.25rem;
position: relative;
padding-left: .75rem;
}
.drag-container {
@include absolute(1.6rem, auto, auto, .75rem);
line-height: 1px;
.plain {
background: none;
border: 0;
border-radius: 0;
.table-items-row-details {
background: $color-white;
border: 1px solid $color-border;
border-radius: $border-radius;
}
.btn-expand.expanded::before {
border-bottom-color: $color-border !important;
}
}
.col {
@ -39,27 +48,29 @@ $padding: 1rem;
position: relative;
}
.nested-field {
position: relative;
&-add {
border: 2px dashed $field-line;
padding: 1rem;
:host ::ng-deep {
.nested-field {
position: relative;
}
&-line-v {
@include absolute($padding, auto, 3 * $padding + .25rem, $padding);
border: 0;
border-left: 2px dashed $field-line;
width: 2px;
}
&-line-h {
@include absolute(-2px, auto, 50%, -$padding);
border: 0;
border-bottom: 2px dashed $field-line;
width: $padding - .25rem;
&-add {
padding-top: 1rem;
padding-bottom: 1rem;
position: relative;
}
&-line-v {
@include absolute($padding, auto, 3 * $padding + .25rem, $padding);
border: 0;
border-left: 2px dashed $field-line;
width: 2px;
}
&-line-h {
@include absolute(-2px, auto, 50%, -$padding);
border: 0;
border-bottom: 2px dashed $field-line;
width: $padding - .25rem;
}
}
}

19
frontend/src/app/features/schemas/pages/schema/fields/field.component.ts

@ -5,9 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { AppSettingsDto, createProperties, DialogModel, EditFieldForm, LanguageDto, ModalModel, NestedFieldDto, RootFieldDto, SchemaDto, SchemasState, sorted } from '@app/shared';
import { AppSettingsDto, createProperties, DialogModel, EditFieldForm, FieldDto, LanguageDto, ModalModel, NestedFieldDto, RootFieldDto, SchemaDto, SchemasState } from '@app/shared';
@Component({
selector: 'sqx-field[field][languages][schema][settings]',
@ -21,6 +20,9 @@ export class FieldComponent implements OnChanges {
@Input()
public schema!: SchemaDto;
@Input()
public plain = false;
@Input()
public parent?: RootFieldDto;
@ -32,14 +34,12 @@ export class FieldComponent implements OnChanges {
public dropdown = new ModalModel();
public trackByFieldFn: (_index: number, field: NestedFieldDto) => any;
public isEditing = false;
public isEditable?: boolean | null;
public editForm!: EditFieldForm;
public addFieldDialog = new DialogModel();
public fieldWizard = new DialogModel();
public get isLocalizable() {
return (this.parent && this.parent.isLocalizable) || this.field['isLocalizable'];
@ -48,7 +48,6 @@ export class FieldComponent implements OnChanges {
constructor(
private readonly schemasState: SchemasState,
) {
this.trackByFieldFn = this.trackByField.bind(this);
}
public ngOnChanges(changes: SimpleChanges) {
@ -88,8 +87,8 @@ export class FieldComponent implements OnChanges {
this.schemasState.hideField(this.schema, this.field);
}
public sortFields(event: CdkDragDrop<ReadonlyArray<NestedFieldDto>>) {
this.schemasState.orderFields(this.schema, sorted(event), this.field as any).subscribe();
public sortFields(fields: ReadonlyArray<FieldDto>) {
this.schemasState.orderFields(this.schema, fields, this.field as any);
}
public lockField() {
@ -117,8 +116,4 @@ export class FieldComponent implements OnChanges {
});
}
}
public trackByField(_index: number, field: NestedFieldDto) {
return field.fieldId + this.schema.id;
}
}

28
frontend/src/app/features/schemas/pages/schema/fields/schema-fields.component.html

@ -1,35 +1,33 @@
<div class="table-items-row table-items-row-summary table-items-row-empty" *ngIf="schema && schema.fields.length === 0">
{{ 'schemas.field.empty' | sqxTranslate }}
<button type="button" class="btn btn-success btn-sm ms-2" (click)="addFieldDialog.show()" *ngIf="schema.canAddField">
<button type="button" class="btn btn-success btn-sm ms-2" (click)="fieldWizard.show()" *ngIf="schema.canAddField">
<i class="icon icon-plus"></i> {{ 'schemas.addField' | sqxTranslate }}
</button>
</div>
<ng-container *ngIf="appsState.selectedSettings| async; let settings">
<ng-container *ngIf="languageState.isoLanguages | async; let languages">
<div
cdkDropList
[cdkDropListDisabled]="!schema.canOrderFields"
[cdkDropListData]="schema.fields"
(cdkDropListDropped)="sortFields($event)">
<div *ngFor="let field of schema.fields; trackBy: trackByFieldFn" class="table-drag" cdkDrag cdkDragLockAxis="y">
<sqx-field [field]="$any(field)" [schema]="schema" [settings]="settings" [languages]="languages">
<i cdkDragHandle class="icon-drag2 drag-handle"></i>
</sqx-field>
</div>
</div>
<sqx-sortable-field-list
[fields]="schema.fields"
[fieldsEmpty]="schema.fields.length === 0"
[languages]="languages"
[schema]="schema"
[settings]="settings"
[sortable]="schema.canOrderFields"
(sorted)="sortFields($event)">
</sqx-sortable-field-list>
<button type="button" class="btn btn-success field-button" (click)="addFieldDialog.show()" *ngIf="schema.canAddField">
<button type="button" class="btn btn-success field-button" (click)="fieldWizard.show()" *ngIf="schema.canAddField">
<i class="icon icon-plus field-button-icon"></i> <div class="field-button-text">{{ 'schemas.addFieldButton' | sqxTranslate }}</div>
</button>
</ng-container>
<ng-container *sqxModal="addFieldDialog">
<ng-container *sqxModal="fieldWizard">
<sqx-field-wizard
[settings]="settings"
[schema]="schema"
(complete)="addFieldDialog.hide()">
(complete)="fieldWizard.hide()">
</sqx-field-wizard>
</ng-container>
</ng-container>

19
frontend/src/app/features/schemas/pages/schema/fields/schema-fields.component.ts

@ -5,9 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { Component, Input, OnInit } from '@angular/core';
import { AppsState, DialogModel, FieldDto, fieldTypes, LanguagesState, RootFieldDto, SchemaDto, SchemasState, sorted } from '@app/shared';
import { AppsState, DialogModel, FieldDto, fieldTypes, LanguagesState, SchemaDto, SchemasState } from '@app/shared';
@Component({
selector: 'sqx-schema-fields[schema]',
@ -15,32 +14,24 @@ import { AppsState, DialogModel, FieldDto, fieldTypes, LanguagesState, RootField
templateUrl: './schema-fields.component.html',
})
export class SchemaFieldsComponent implements OnInit {
public fieldTypes = fieldTypes;
@Input()
public schema!: SchemaDto;
public addFieldDialog = new DialogModel();
public trackByFieldFn: (_index: number, field: FieldDto) => any;
public fieldTypes = fieldTypes;
public fieldWizard = new DialogModel();
constructor(
public readonly appsState: AppsState,
public readonly schemasState: SchemasState,
public readonly languageState: LanguagesState,
) {
this.trackByFieldFn = this.trackByField.bind(this);
}
public ngOnInit() {
this.languageState.load();
}
public sortFields(event: CdkDragDrop<ReadonlyArray<RootFieldDto>>) {
this.schemasState.orderFields(this.schema, sorted(event)).subscribe();
}
public trackByField(_index: number, field: FieldDto) {
return field.fieldId + this.schema.id;
public sortFields(fields: ReadonlyArray<FieldDto>) {
this.schemasState.orderFields(this.schema, fields).subscribe();
}
}

2
frontend/src/app/framework/angular/drag-helper.ts

@ -8,7 +8,7 @@
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { Types } from './../utils/types';
export function sorted<T>(event: CdkDragDrop<ReadonlyArray<T>>): ReadonlyArray<T> {
export function sorted<T>(event: CdkDragDrop<ReadonlyArray<T>>): T[] {
const items = <T[]>event.container.data;
moveItemInArray(items, event.previousIndex, event.currentIndex);

10
frontend/src/app/shared/state/contents.forms-helpers.ts

@ -99,8 +99,10 @@ export abstract class Hidden {
}
}
export function groupFields<T extends FieldDto>(fields: ReadonlyArray<T>): { separator?: T; fields: ReadonlyArray<T> }[] {
const result: { separator?: T; fields: ReadonlyArray<T> }[] = [];
export type FieldGroup<T = FieldDto> = { separator?: T; fields: T[] };
export function groupFields<T extends FieldDto>(fields: ReadonlyArray<T>, keepEmpty = false): FieldGroup<T>[] {
const result: FieldGroup<T>[] = [];
let currentSeparator: T | undefined;
let currentFields: T[] = [];
@ -109,7 +111,7 @@ export function groupFields<T extends FieldDto>(fields: ReadonlyArray<T>): { sep
if (field.properties.isContentField) {
currentFields.push(field);
} else {
if (currentFields.length > 0) {
if (currentFields.length > 0 || keepEmpty) {
result.push({ separator: currentSeparator, fields: currentFields });
}
@ -118,7 +120,7 @@ export function groupFields<T extends FieldDto>(fields: ReadonlyArray<T>): { sep
}
}
if (currentFields.length > 0) {
if (currentFields.length > 0 || keepEmpty) {
result.push({ separator: currentSeparator, fields: currentFields });
}

2
frontend/src/app/shared/state/schemas.state.ts

@ -266,7 +266,7 @@ export class SchemasState extends State<Snapshot> {
shareSubscribed(this.dialogs));
}
public orderFields(schema: SchemaDto, fields: ReadonlyArray<any>, parent?: RootFieldDto): Observable<SchemaDto> {
public orderFields(schema: SchemaDto, fields: ReadonlyArray<FieldDto>, parent?: RootFieldDto): Observable<SchemaDto> {
return this.schemasService.putFieldOrdering(this.appName, parent || schema, fields.map(t => t.fieldId), schema.version).pipe(
tap(updated => {
this.replaceSchema(updated, schema.version, 'i18n:schemas.saved');

1
frontend/src/app/shared/state/settings.ts

@ -21,6 +21,7 @@ export const Settings = {
DISABLE_ONBOARDING: (key: any) => `squidex.onboarding.disable.${key}`,
FIELD_ALL: (schema: any, field: any) => `squidex.schemas.${schema}.fields.${field}.show-all`,
FIELD_COLLAPSED: (schema: any, field: any) => `squidex.schemas.${schema}.fields.${field}.closed`,
FIELD_EDITOR_COLLAPSED: (schema: any, field: any) => `squidex.schemas.${schema}.editor.fields.${field}.closed`,
HIDE_MAP: 'hideMap',
NEWS_VERSION: 'squidex.news.version',
NOTIFICATION_VERSION: 'notifications.version',

Loading…
Cancel
Save