swimlane/ngx-ui

View on GitHub
projects/swimlane/ngx-ui/src/lib/components/json-editor/json-editor-flat/json-editor-node-flat/node-types/object-node-flat/object-node-flat.component.ts

Summary

Maintainability
A
3 hrs
Test Coverage
import {
  Component,
  ViewEncapsulation,
  Input,
  ViewChild,
  TemplateRef,
  OnInit,
  ChangeDetectionStrategy,
  ChangeDetectorRef,
  SimpleChanges,
  OnChanges
} from '@angular/core';
import { ObjectNode } from '../../../../node-types/object-node.component';
import { DialogService } from '../../../../../dialog/dialog.service';
import {
  JsonSchemaDataType,
  jsonSchemaDataTypes,
  JSONEditorSchema,
  createValueForSchema
} from '../../../../json-editor.helper';
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { PropertyConfigOptions, PropertyConfigComponent } from '../property-config/property-config.component';

@Component({
  selector: 'ngx-json-object-node-flat',
  templateUrl: './object-node-flat.component.html',
  styleUrls: ['./object-node-flat.component.scss'],
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ObjectNodeFlatComponent extends ObjectNode implements OnInit, OnChanges {
  @ViewChild('propertyConfigTmpl', { static: false }) propertyConfigTmpl: TemplateRef<PropertyConfigComponent>;

  @Input() level: number;

  @Input() schemaBuilderMode: boolean;

  @Input() formats: JsonSchemaDataType[] = [];

  @Input() hideRoot = false;

  @Input() isDuplicated = false;

  @Input() passwordToggleEnabled = false;

  @Input() inputControlTemplate: TemplateRef<unknown>;

  indentationArray: number[] = [];

  duplicatedFields = new Map<string, string>();

  objectKeys = Object.keys;

  get isRoot() {
    return (this.hideRoot && this.level === -1) || (!this.hideRoot && this.level === 0);
  }

  get indentAdd() {
    return this.hideRoot && this.level === 0;
  }

  constructor(private dialogService: DialogService, protected cdr: ChangeDetectorRef) {
    super(cdr);
  }

  ngOnInit() {
    if (this.schemaBuilderMode) {
      this.dataTypes = [...jsonSchemaDataTypes, ...this.formats];
    }

    setTimeout(() => {
      this.initSchemaProperties(this.schema);
      this.initSchemaProperties(this.schemaRef);
    });

    this.indentationArray = this.level > 0 ? Array(this.level).fill(this.level) : [];
  }

  ngOnChanges(changes: SimpleChanges) {
    super.ngOnChanges(changes);
    if ('level' in changes) {
      this.indentationArray = this.level > 0 ? Array(this.level).fill(this.level) : [];
    }
  }

  onUpdatePropertyName(options: { id: string; name: string }): void {
    let schema = this.schemaRef || this.schema;

    schema ||= {};
    schema.properties ||= {};

    const existingSchemaProperty = schema.properties[options.name];
    const existingPropertyValue = this.model[options.name];
    const oldName = this.propertyIndex[options.id].propertyName;

    this.duplicatedFields.delete(options.id);

    if (!existingSchemaProperty && existingPropertyValue === undefined) {
      const index = Object.keys(schema.properties).indexOf(oldName);
      this.updateSchemaPropertyName(schema, options.name, this.propertyIndex[options.id].propertyName);
      if (this.schemaBuilderMode) this.swapSchemaProperties(index);
      this.updatePropertyName(options.id, options.name);
      this.schemaUpdate.emit();
    } else if (oldName !== options.name) {
      this.duplicatedFields.set(options.id, options.name);
    }
  }

  onPropertyConfig(property: JSONEditorSchema, index: number, isNew = false): void {
    const dialog = this.dialogService.create({
      template: this.propertyConfigTmpl,
      context: {
        property,
        index,
        schema: this.schema,
        formats: this.formats,
        isNew,
        apply: (options: PropertyConfigOptions) => {
          dialog.destroy();
          this.updateSchemaProperty(options);
        }
      },
      class: 'property-config-dialog'
    });
  }

  updateSchemaProperty(options: PropertyConfigOptions): void {
    const oldProperty = options.oldProperty;
    const newProperty = options.newProperty;

    const oldName = oldProperty.propertyName;
    const newName = newProperty.propertyName;

    if (newName !== oldName) {
      const schema = this.schemaBuilderMode ? this.schemaRef : this.schema;
      this.updateSchemaPropertyName(schema, newName, oldName);
      this.updatePropertyName(options.newProperty.id, newName);
    }

    this.toggleRequiredValue(options.required, newName);

    this.schema.properties ||= {};
    this.schema.properties[newName] = newProperty;
    this.propertyIndex[options.newProperty.id] = newProperty;

    if (this.schemaBuilderMode) this.updateSchemaRefProperty(newProperty);

    if (newName !== oldName) {
      this.swapSchemaProperties(options.index);
    }

    if (oldProperty.type !== newProperty.type) {
      const value: any = createValueForSchema(newProperty);
      this.model[newProperty.propertyName] = value;
    }

    this.propertyIndex = { ...this.propertyIndex };
    this.schemaUpdate.emit();
  }

  addProperty(dataType: JsonSchemaDataType): void {
    super.addProperty(dataType);

    const index = this.propertyId - 1;
    const property = this.propertyIndex[index];
    this.updateSchemaRefProperty(property);
    this.schemaUpdate.emit();

    if (this.schemaBuilderMode) {
      this.onPropertyConfig(property, index, true);
    }
  }

  deleteProperty(propName: string): void {
    delete this.schemaRef.properties[propName];
    if (this.schemaBuilderMode) {
      delete this.schema.properties[propName];
      this.toggleRequiredValue(false, propName);
    } else if (!this.schema.required?.includes(propName) && !(propName in this.schema.properties)) {
      delete this.schemaRef.properties[propName];
    }

    this.schemaUpdate.emit();
    super.deleteProperty(propName);
  }

  drop(event: CdkDragDrop<string[]>): void {
    const propertyIndexValues = Object.values(this.propertyIndex);

    moveItemInArray(propertyIndexValues, event.previousIndex, event.currentIndex);

    let index = 0;
    for (const prop in this.propertyIndex) {
      this.propertyIndex[prop] = propertyIndexValues[index];
      this.propertyIndex[prop].id = parseInt(prop, 10);
      index += 1;
    }

    this.propertyIndex = { ...this.propertyIndex };

    this.swapSchemaProperties(event.currentIndex, event.previousIndex);
  }

  private swapSchemaProperties(currentIndex: number, previousIndex?: number): void {
    const propertiesIds = Object.keys(this.schemaRef.properties);

    if (previousIndex === undefined) {
      previousIndex = propertiesIds.length - 1;
    }

    moveItemInArray(propertiesIds, previousIndex, currentIndex);

    this.schemaRef.properties = propertiesIds.reduce((result, prop) => {
      result[prop] = this.schemaRef.properties[prop];
      return result;
    }, {});

    this.schemaUpdate.emit();
  }

  private initSchemaProperties(schema: JSONEditorSchema): void {
    if (schema) {
      schema.required = schema.required || [];
      schema.properties = schema.properties || {};
    }
  }

  private updateSchemaRefProperty(prop: any): void {
    this.schemaRef.properties ||= Object.create(null);
    this.schemaRef.properties[prop.propertyName] = {
      type: prop.type,
      ...(prop.format && { format: prop.format }),
      ...(prop.examples && { examples: prop.examples }),
      ...(prop.title && { title: prop.title }),
      ...(prop.items && { items: prop.items }),
      ...(prop.required && { required: prop.required }),
      ...(prop.properties && { properties: prop.properties }),
      ...(prop.enum && { enum: prop.enum }),
      ...(prop.default && { default: prop.default }),
      ...(prop.description && { description: prop.description }),
      ...(prop.nameEditable && { nameEditable: prop.nameEditable }),
      ...(prop.minimum && { minimum: prop.minimum }),
      ...(prop.maximum && { maximum: prop.maximum }),
      ...(prop.minLength && { minLength: prop.minLength }),
      ...(prop.maxLength && { maxLength: prop.maxLength }),
      ...(prop.minItems && { minItems: prop.minItems }),
      ...(prop.maxItems && { maxItems: prop.maxItems }),
      ...(prop.pattern && { pattern: prop.pattern })
    };
  }

  private updateSchemaPropertyName(schema: JSONEditorSchema, newName: string, oldName: string): void {
    this.updateRequiredProperties(schema, newName, oldName);
    schema ||= {};
    schema.properties ||= {};
    schema.properties[newName] = schema.properties[oldName];
    delete schema.properties[oldName];
  }

  private toggleRequiredValue(required: boolean, propertyName: string): void {
    const requiredIndex = this.schema.required.indexOf(propertyName);
    if (requiredIndex >= 0 && !required) {
      this.schema.required.splice(requiredIndex, 1);
    } else if (requiredIndex < 0 && required) {
      this.schema.required.push(propertyName);
    }

    this.schemaRef.required = [...this.schema.required];
    this.updateRequiredCache();
  }

  private updateRequiredProperties(schema: JSONEditorSchema, newName: string, oldName: string): void {
    schema ||= {};
    schema.required ||= [];
    const requiredIndex = schema.required.indexOf(oldName);
    if (requiredIndex >= 0) {
      schema.required[requiredIndex] = newName;
    }
  }
}