Aam-Digital/ndb-core

View on GitHub
src/app/core/entity-details/related-entities/related-entities.component.ts

Summary

Maintainability
A
0 mins
Test Coverage
A
94%
import { Component, Input, OnInit } from "@angular/core";
import { DynamicComponent } from "../../config/dynamic-components/dynamic-component.decorator";
import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service";
import { Entity, EntityConstructor } from "../../entity/model/entity";
import { EntityRegistry } from "../../entity/database-entity.decorator";
import { EntitiesTableComponent } from "../../common-components/entities-table/entities-table.component";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
import { applyUpdate } from "../../entity/model/entity-update";
import {
  ScreenSize,
  ScreenWidthObserver,
} from "../../../utils/media/screen-size-observer.service";
import {
  ColumnConfig,
  FormFieldConfig,
  toFormFieldConfig,
} from "../../common-components/entity-form/FormConfig";
import { DataFilter } from "../../filter/filters/filters";
import { FilterService } from "../../filter/filter.service";
import { EntityDatatype } from "../../basic-datatypes/entity/entity.datatype";

/**
 * Load and display a list of entity subrecords (entities related to the current entity details view).
 */
@DynamicComponent("RelatedEntities")
@UntilDestroy()
@Component({
  selector: "app-related-entities",
  templateUrl: "./related-entities.component.html",
  standalone: true,
  imports: [EntitiesTableComponent],
})
export class RelatedEntitiesComponent<E extends Entity> implements OnInit {
  /** currently viewed/main entity for which related entities are displayed in this component */
  @Input() entity: Entity;

  /** entity type of the related entities to be displayed */
  @Input() set entityType(value: string) {
    this.entityCtr = this.entityRegistry.get(value) as EntityConstructor<E>;
  }

  /**
   * Property name of the related entities (type given in this.entityType) that holds the entity id
   * to be matched with the id of the current main entity (given in this.entity).
   * If not explicitly set, this will be inferred based on the defined relations between the entities.
   *
   * manually setting this is only necessary if you have multiple properties referencing the same entity type
   * and you want to list only records related to one of them.
   * For example: if you set `entityType = "Project"` (to display a list of projects here) and the Project entities have a properties "participants" and "supervisors" both storing references to User entities,
   * you can set `property = "supervisors"` to only list those projects where the current User is supervisors, not participant.
   */
  @Input() property: string | string[];

  /**
   * Columns to be displayed in the table
   * @param value
   */
  @Input()
  public set columns(value: ColumnConfig[]) {
    if (!Array.isArray(value)) {
      return;
    }

    this._columns = value.map((c) => toFormFieldConfig(c));
    this.updateColumnsToDisplayForScreenSize();
  }
  protected _columns: FormFieldConfig[];

  columnsToDisplay: string[];

  /**
   * This filter is applied before displaying the data.
   */
  @Input() filter?: DataFilter<E>;

  /**
   * Whether inactive/archived records should be shown.
   */
  @Input() showInactive: boolean;

  @Input() clickMode: "popup" | "navigate" = "popup";
  @Input() editable: boolean = true;

  data: E[];
  protected entityCtr: EntityConstructor<E>;

  constructor(
    protected entityMapper: EntityMapperService,
    private entityRegistry: EntityRegistry,
    private screenWidthObserver: ScreenWidthObserver,
    protected filterService: FilterService,
  ) {
    this.screenWidthObserver
      .shared()
      .pipe(untilDestroyed(this))
      .subscribe(() => this.updateColumnsToDisplayForScreenSize());
  }

  async ngOnInit() {
    this.property = this.property ?? this.getProperty();
    this.data = await this.getData();
    this.filter = this.initFilter();

    if (this.showInactive === undefined) {
      // show all related docs when visiting an archived entity
      this.showInactive = this.entity.anonymized;
    }

    this.listenToEntityUpdates();
  }

  protected getData(): Promise<E[]> {
    return this.entityMapper.loadType(this.entityCtr);
  }

  protected getProperty(): string | string[] {
    const relType = this.entity.getType();
    const found = [...this.entityCtr.schema].filter(([, field]) => {
      const entityDatatype = field.dataType === EntityDatatype.dataType;
      return entityDatatype && Array.isArray(field.additional)
        ? field.additional.includes(relType)
        : field.additional === relType;
    });
    return found.length === 1 ? found[0][0] : found.map(([key]) => key);
  }

  protected initFilter(): DataFilter<E> {
    const filter: DataFilter<E> = { ...this.filter };

    if (this.property) {
      // only show related entities
      if (typeof this.property === "string") {
        Object.assign(filter, this.getFilterForProperty(this.property));
      } else if (this.property.length > 0) {
        filter["$or"] = this.property.map((prop) =>
          this.getFilterForProperty(prop),
        );
      }
    }

    return filter;
  }

  private getFilterForProperty(property: string) {
    const isArray = this.entityCtr.schema.get(property).isArray;
    const filter = isArray
      ? { $elemMatch: { $eq: this.entity.getId() } }
      : this.entity.getId();
    return { [property]: filter };
  }

  protected listenToEntityUpdates() {
    this.entityMapper
      .receiveUpdates(this.entityCtr)
      .pipe(untilDestroyed(this))
      .subscribe((next) => {
        this.data = applyUpdate(this.data, next);
      });
  }

  createNewRecordFactory() {
    return () => {
      const rec = new this.entityCtr();
      this.filterService.alignEntityWithFilter(rec, this.filter);
      return rec;
    };
  }

  private updateColumnsToDisplayForScreenSize() {
    if (!this._columns) {
      return;
    }

    this.columnsToDisplay = this._columns
      .filter((column) => {
        if (column?.hideFromTable) {
          return false;
        }

        const numericValue = ScreenSize[column?.visibleFrom];
        if (numericValue === undefined) {
          return true;
        }
        return this.screenWidthObserver.currentScreenSize() >= numericValue;
      })
      .map((c) => c.id);
  }
}