opf/openproject

View on GitHub
frontend/src/app/features/invite-user-modal/principal/principal.component.ts

Summary

Maintainability
B
4 hrs
Test Coverage
import {
  ChangeDetectionStrategy,
  ChangeDetectorRef,
  Component,
  EventEmitter,
  Input,
  OnInit,
  Output,
  ViewChild,
} from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {
  AbstractControl,
  UntypedFormControl,
  UntypedFormGroup,
  Validators,
} from '@angular/forms';
import { take } from 'rxjs/internal/operators/take';
import { map } from 'rxjs/operators';
import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { DynamicFormComponent } from 'core-app/shared/components/dynamic-forms/components/dynamic-form/dynamic-form.component';
import {
  PrincipalData,
  PrincipalLike,
} from 'core-app/shared/components/principal/principal-types';
import { ProjectResource } from 'core-app/features/hal/resources/project-resource';
import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { PrincipalType } from '../invite-user.component';
import { RoleResource } from 'core-app/features/hal/resources/role-resource';

function extractCustomFieldsFromSchema(schema:IOPFormSettings['_embedded']['schema']) {
  return Object.keys(schema)
    .reduce((fields, name) => {
      if (name.startsWith('customField') && schema[name].required) {
        return {
          ...fields,
          [name]: schema[name],
        };
      }

      return fields;
    }, {});
}

@Component({
  selector: 'op-ium-principal',
  templateUrl: './principal.component.html',
  styleUrls: ['./principal.component.sass'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PrincipalComponent implements OnInit {
  @Input() principalData:PrincipalData;

  @Input() project:ProjectResource;

  @Input() type:PrincipalType;

  @Input() roleData:RoleResource;

  @Input() messageData = '';

  @Output() close = new EventEmitter<void>();

  @Output() save = new EventEmitter<{
    principalData:PrincipalData,
    isAlreadyMember:boolean,
    role:RoleResource,
    message:string
  }>();

  @Output() back = new EventEmitter();

  @ViewChild(DynamicFormComponent) dynamicForm:DynamicFormComponent;

  public PrincipalType = PrincipalType;

  public text = {
    principal: {
      title: ():string => this.I18n.t('js.invite_user_modal.title.invite'),
      label: {
        User: this.I18n.t('js.invite_user_modal.principal.label.name_or_email'),
        PlaceholderUser: this.I18n.t('js.invite_user_modal.principal.label.name'),
        Group: this.I18n.t('js.invite_user_modal.principal.label.name'),
        Email: this.I18n.t('js.label_email'),
      },
      change: this.I18n.t('js.label_change'),
      inviteUser: this.I18n.t('js.invite_user_modal.principal.invite_user'),
      createNewPlaceholder: this.I18n.t('js.invite_user_modal.principal.create_new_placeholder'),
      required: {
        User: this.I18n.t('js.invite_user_modal.principal.required.user'),
        PlaceholderUser: this.I18n.t('js.invite_user_modal.principal.required.placeholder'),
        Group: this.I18n.t('js.invite_user_modal.principal.required.group'),
      },
      backButton: this.I18n.t('js.invite_user_modal.back'),
      nextButton: this.I18n.t('js.invite_user_modal.principal.next_button'),
      cancelButton: this.I18n.t('js.button_cancel'),
    },
    role: {
      label: ():string => this.I18n.t('js.invite_user_modal.role.label', {
        project: this.project?.name,
      }),
      description: ():string => this.I18n.t('js.invite_user_modal.role.description', {
        principal: this.principal?.name,
      }),
      required: this.I18n.t('js.invite_user_modal.role.required'),
    },
    message: {
      label: this.I18n.t('js.invite_user_modal.message.label'),
      description: ():string => this.I18n.t('js.invite_user_modal.message.description', {
        principal: this.principal?.name,
      }),
    },
  };

  public principalForm = new UntypedFormGroup({
    // eslint-disable-next-line @typescript-eslint/unbound-method
    principal: new UntypedFormControl(null, [Validators.required]),
    userDynamicFields: new UntypedFormGroup({}),
    // eslint-disable-next-line @typescript-eslint/unbound-method
    role: new UntypedFormControl(null, [Validators.required]),
    message: new UntypedFormControl(''),
  });

  public userDynamicFieldConfig:{
    payload:IOPFormSettings['_embedded']['payload']|null,
    schema:IOPFormSettings['_embedded']['schema']|null,
  } = {
    payload: null,
    schema: null,
  };

  get messageControl():AbstractControl|null {
    return this.principalForm.get('message');
  }

  get roleControl():AbstractControl|null {
    return this.principalForm.get('role');
  }

  get principalControl():AbstractControl|null {
    return this.principalForm.get('principal');
  }

  get principal():PrincipalLike|undefined {
    return this.principalControl?.value as PrincipalLike|undefined;
  }

  get role():RoleResource|undefined {
    return this.roleControl?.value as RoleResource|undefined;
  }

  get message():string|undefined {
    return this.messageControl?.value as string|undefined;
  }

  get dynamicFieldsControl():AbstractControl|null {
    return this.principalForm.get('userDynamicFields');
  }

  get customFields():{ [key:string]:any } {
    return this.dynamicFieldsControl?.value;
  }

  get hasPrincipalSelected():boolean {
    return !!this.principal;
  }

  get textLabel():string {
    if (this.type === PrincipalType.User && this.isNewPrincipal) {
      return this.text.principal.label.Email;
    }
    return this.text.principal.label[this.type];
  }

  get isNewPrincipal():boolean {
    return this.hasPrincipalSelected && !(this.principal instanceof HalResource);
  }

  get isMemberOfCurrentProject():boolean {
    return !!this.principalControl?.value?.memberships?.elements?.find((mem:any) => mem.project.id === this.project.id);
  }

  constructor(
    readonly I18n:I18nService,
    readonly httpClient:HttpClient,
    readonly apiV3Service:ApiV3Service,
    readonly cdRef:ChangeDetectorRef,
  ) {}

  ngOnInit():void {
    this.principalControl?.setValue(this.principalData.principal);
    this.roleControl?.setValue(this.roleData);
    this.messageControl?.setValue(this.messageData);

    if (this.type === PrincipalType.User) {
      const payload = this.isNewPrincipal ? this.principalData.customFields : {};
      this
        .apiV3Service
        .users
        .form
        .post(payload)
        .pipe(
          take(1),
          // The subsequent code expects to not work with a HalResource but rather with the raw
          // api response.
          map((formResource) => formResource.$source),
        )
        .subscribe((formConfig) => {
          this.userDynamicFieldConfig.schema = extractCustomFieldsFromSchema(formConfig._embedded?.schema);
          this.userDynamicFieldConfig.payload = formConfig._embedded?.payload;
          this.cdRef.detectChanges();
        });
    }
  }

  createNewFromInput(input:PrincipalLike):void {
    this.principalControl?.setValue(input);
  }

  onSubmit($e:Event):void {
    $e.preventDefault();

    if (this.dynamicForm) {
      this.dynamicForm.validateForm().subscribe(() => {
        this.onValidatedSubmit();
      });
    } else {
      this.onValidatedSubmit();
    }
  }

  onValidatedSubmit():void {
    if (this.principalForm.invalid) {
      return;
    }

    // The code below transforms the model value as it comes from the dynamic form to the value accepted by the API.
    // This is not just necessary for submit, but also so that we can reseed the initial values to the payload
    // when going back to this step after having completed it once.
    const fieldsSchema = this.userDynamicFieldConfig.schema || {};
    const customFields = Object.keys(fieldsSchema)
      .reduce((result, fieldKey) => {
        const fieldSchema = fieldsSchema[fieldKey];
        let fieldValue = this.customFields[fieldKey];

        if (fieldSchema.location === '_links') {
          fieldValue = Array.isArray(fieldValue)
            ? fieldValue.map((opt:any) => (opt._links ? opt._links.self : opt))
            : (fieldValue._links ? fieldValue._links.self : fieldValue);
        }

        result = {
          ...result,
          [fieldKey]: fieldValue,
        };

        return result;
      }, {});

    this.save.emit({
      principalData: {
        customFields,
        principal: this.principal as PrincipalLike,
      },
      isAlreadyMember: this.isMemberOfCurrentProject,
      role: this.role as RoleResource,
      message: this.message as string,
    });
  }
}