opf/openproject

View on GitHub
frontend/src/app/features/work-packages/components/wp-edit-form/work-package-filter-values.ts

Summary

Maintainability
A
35 mins
Test Coverage
import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { CurrentUserService } from 'core-app/core/current-user/current-user.service';
import { HalResourceService } from 'core-app/features/hal/services/hal-resource.service';
import { Injector } from '@angular/core';
import { compareByHrefOrString } from 'core-app/shared/helpers/angular/tracking-functions';
import { WorkPackageChangeset } from 'core-app/features/work-packages/components/wp-edit/work-package-changeset';
import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
import { FilterOperator } from 'core-app/shared/helpers/api-v3/api-v3-filter-builder';
import { QueryFilterInstanceResource } from 'core-app/features/hal/resources/query-filter-instance-resource';
import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';

export class WorkPackageFilterValues {
  @InjectField() currentUser:CurrentUserService;

  @InjectField() halResourceService:HalResourceService;

  @InjectField() currentProject:CurrentProjectService;

  handlers:Partial<Record<FilterOperator, (change:WorkPackageChangeset|{ [id:string]:unknown }, filter:QueryFilterInstanceResource) => void>> = {
    '=': this.applyFirstValue.bind(this),
    '!*': this.setToNull.bind(this),
  };

  constructor(
    public injector:Injector,
    private filters:QueryFilterInstanceResource[],
    private excluded:string[] = [],
  ) {}

  applyDefaultsFromFilters(change:WorkPackageChangeset|{ [id:string]:unknown }):void {
    _.each(this.filters, (filter) => {
      // Exclude filters specified in constructor
      if (this.excluded.indexOf(filter.id) !== -1) {
        return;
      }
      const operator = filter.operator.id as FilterOperator;

      // Special case due to the introduction of the project include dropdown
      // If we are in a project, we want the create wp to be part of that project.
      // Only for embedded tables, there might be different filter values necessary.
      if (filter.id === 'project') {
        if (operator !== '=') return;

        const projectFilter = _.find(filter.values, (resource:HalResource|string) => {
          return ((resource instanceof HalResource) ? resource.href : resource) === this.currentProject.apiv3Path;
        });
        this.setValue(change, 'project', projectFilter || filter.values[0]);

        return;
      }

      // ID filters should never be taken over
      if (filter.id === 'id') {
        return;
      }

      // Look for a handler with the filter's operator
      const handler = this.handlers[operator];

      // Apply the filter if there is any
      handler?.call(this, change, filter);
    });
  }

  /**
   * Apply a positive value from a '=' [value] filter
   *
   * @param filter A positive '=' filter with at least one value
   * @private
   */
  private applyFirstValue(change:WorkPackageChangeset|{ [id:string]:unknown }, filter:QueryFilterInstanceResource):void {
    // Avoid setting a value if current value is in filter list
    // and more than one value selected
    if (this.filterAlreadyApplied(change, filter)) {
      return;
    }

    // Select the first value
    const value = filter.values[0];

    // Avoid empty values
    if (value) {
      const attributeName = this.mapFilterToAttribute(filter);
      this.setValueFor(change, attributeName, value);
    }
  }

  /**
   * Set a value no null for a none type filter (!*)
   *
   * @param change changeset or resource
   * @param filter A none '!*' filter
   * @private
   */
  private setToNull(change:WorkPackageChangeset|{ [id:string]:unknown }, filter:QueryFilterInstanceResource):void {
    const attributeName = this.mapFilterToAttribute(filter);

    this.setValue(change, attributeName, { href: null });
  }

  private setValueFor(change:WorkPackageChangeset|{ [id:string]:unknown }, field:string, value:string|HalResource):void {
    const newValue = this.findSpecialValue(value, field) || value;

    if (newValue) {
      this.setValue(change, field, newValue);
    }
  }

  private setValue(change:WorkPackageChangeset|{ [id:string]:unknown }, field:string, value:unknown):void {
    if (change instanceof WorkPackageChangeset) {
      change.setValue(field, value);
    } else {
      change[field] = value;
    }
  }

  /**
   * Returns special values for which no allowed values exist (e.g., parent ID in embedded queries)
   * @param {string | HalResource} value
   * @param {string} field
   */
  private findSpecialValue(value:string|HalResource, field:string):string|HalResource|undefined {
    if (field === 'parent') {
      return value;
    }

    if (value instanceof HalResource && value.href === '/api/v3/users/me' && this.currentUser.isLoggedIn) {
      return this.halResourceService.fromSelfLink(`/api/v3/users/${this.currentUser.userId}`);
    }

    return undefined;
  }

  /**
   * Avoid applying filter values when changeset already matches one of the selected values
   * @param filter
   */
  private filterAlreadyApplied(change:WorkPackageChangeset|{ [id:string]:unknown }, filter:{ id:string, values:unknown[] }):boolean {
    const value:unknown = change instanceof WorkPackageChangeset ? change.projectedResource[filter.id] : change[filter.id];
    const current = _.castArray(value);

    for (let i = 0; i < filter.values.length; i++) {
      for (let j = 0; j < current.length; j++) {
        if (compareByHrefOrString(current[j], filter.values[i])) {
          return true;
        }
      }
    }

    return false;
  }

  /**
   * Some filter ids need to be mapped to a different attribute name
   * in order to be processed correctly.
   *
   * @param filter The filter to map
   * @returns An attribute name string to set
   * @private
   */
  private mapFilterToAttribute(filter:any):string {
    if (filter.id === 'onlySubproject') {
      return 'project';
    }

    // Default to returning the filter id
    return filter.id;
  }
}