ilios/frontend

View on GitHub
packages/frontend/app/components/learner-group/root.js

Summary

Maintainability
A
3 hrs
Test Coverage
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import ObjectProxy from '@ember/object/proxy';
import { service } from '@ember/service';
import { isPresent } from '@ember/utils';
import { all, map } from 'rsvp';
import { dropTask, enqueueTask, restartableTask, task } from 'ember-concurrency';
import pad from 'pad';
import { TrackedAsyncData } from 'ember-async-data';
import { cached } from '@glimmer/tracking';
import { Length, IsURL, validatable } from 'ilios-common/decorators/validation';
import { findById, mapBy, uniqueValues } from 'ilios-common/utils/array-helpers';
import cloneLearnerGroup from '../../utils/clone-learner-group';
import countDigits from '../../utils/count-digits';

const DEFAULT_URL_VALUE = 'https://';

@validatable
export default class LearnerGroupRootComponent extends Component {
  @service flashMessages;
  @service intl;
  @service store;
  @tracked cohortTitle = null;
  @tracked learnerGroupId = null;
  @tracked learnerGroupTitle = null;
  @tracked location = null;
  @IsURL() @Length(2, 2000) @tracked url = null;
  @tracked topLevelGroupTitle = null;
  @tracked showLearnerGroupCalendar = false;
  @tracked courses = [];
  @tracked treeGroups = [];
  @tracked usersToPassToManager = [];
  @tracked usersToPassToCohortManager = [];
  @tracked sortGroupsBy = 'title';
  @tracked isSavingGroups = false;
  @tracked savedGroup;
  @tracked showNewLearnerGroupForm = false;
  @tracked currentGroupsSaved = 0;
  @tracked totalGroupsToSave = 0;

  @cached
  get subGroupsData() {
    return new TrackedAsyncData(this.args.learnerGroup.children);
  }

  get subGroups() {
    return this.subGroupsData.isResolved ? this.subGroupsData.value : null;
  }

  get learnerGroups() {
    if (!this.subGroups) {
      return [];
    }
    return this.subGroups.slice();
  }

  get bestUrl() {
    if (this.url || this.urlChanged) {
      return this.url;
    }

    return DEFAULT_URL_VALUE;
  }

  get sortUsersBy() {
    return this.args.sortUsersBy || 'fullName';
  }

  load = restartableTask(async (element, [learnerGroup]) => {
    if (isPresent(learnerGroup)) {
      this.location = learnerGroup.location;
      this.url = learnerGroup.url;
      this.learnerGroupId = learnerGroup.id;
      this.learnerGroupTitle = learnerGroup.title;
      const cohort = await learnerGroup.cohort;
      this.cohortTitle = cohort.title;
      const topLevelGroup = await learnerGroup.getTopLevelGroup();
      this.topLevelGroupTitle = topLevelGroup.title;
      const allDescendants = await topLevelGroup.getAllDescendants();
      this.treeGroups = [topLevelGroup, ...allDescendants];
      this.usersToPassToManager = await this.createUsersToPassToManager.perform();
      this.usersToPassToCohortManager = await this.createUsersToPassToCohortManager.perform();
      this.courses = await this.getCoursesForGroupWithSubgroupName(null, this.args.learnerGroup);
    }
  });

  saveNewLearnerGroup = dropTask(async (title) => {
    const cohort = await this.args.learnerGroup.cohort;
    const newLearnerGroup = this.store.createRecord('learner-group', {
      cohort,
      parent: this.args.learnerGroup,
      title,
    });
    this.savedGroup = await newLearnerGroup.save();
    this.showNewLearnerGroupForm = false;
  });

  changeLocation = restartableTask(async () => {
    this.addErrorDisplayFor('location');
    const isValid = await this.isValid('location');
    if (!isValid) {
      return false;
    }
    this.removeErrorDisplayFor('location');
    this.args.learnerGroup.set('location', this.location);
    await this.args.learnerGroup.save();
    this.location = this.args.learnerGroup.location;
  });

  @action
  async generateNewLearnerGroups(num) {
    this.savedGroup = null;
    this.currentGroupsSaved = 0;
    this.isSaving = true;
    this.totalGroupsToSave = num;
    const offset = await this.args.learnerGroup.getSubgroupNumberingOffset();
    const cohort = await this.args.learnerGroup.cohort;
    const padBy = countDigits(offset + parseInt(num, 10));
    const parentTitle = this.args.learnerGroup.title.substring(0, 60 - 1 - padBy);
    const groups = [];
    for (let i = 0; i < num; i++) {
      const newGroup = this.store.createRecord('learner-group', {
        cohort,
        parent: this.args.learnerGroup,
        title: `${parentTitle} ${pad(padBy, offset + i, '0')}`,
      });
      groups.push(newGroup);
    }
    const saveSomeGroups = async (groupsToSave) => {
      const chunk = groupsToSave.splice(0, 6);
      await all(chunk.map((group) => group.save()));

      if (groupsToSave.length) {
        this.currentGroupsSaved = this.currentGroupsSaved + chunk.length;
        await saveSomeGroups(groupsToSave);
      } else {
        this.isSaving = false;
        this.flashMessages.success('general.savedSuccessfully');
        this.showNewLearnerGroupForm = false;
      }
    };
    await saveSomeGroups(groups);
  }

  @action
  revertLocationChanges() {
    this.location = this.args.learnerGroup.location;
  }

  @action
  selectAllText({ target }) {
    if (target.value === DEFAULT_URL_VALUE) {
      target.select();
    }
  }

  saveUrlChanges = restartableTask(async () => {
    this.addErrorDisplayFor('url');
    const isValid = await this.isValid('url');
    if (!isValid) {
      return false;
    }
    this.removeErrorDisplayFor('url');
    this.args.learnerGroup.set('url', this.url);
    await this.args.learnerGroup.save();
    this.url = this.args.learnerGroup.url;
  });

  @action
  revertUrlChanges() {
    this.url = this.args.learnerGroup.url;
  }

  @action
  changeUrl(value) {
    value = value.trim();
    const regex = RegExp('https://http[s]?:');
    if (regex.test(value)) {
      value = value.substring(8);
    }
    this.url = value;
    this.urlChanged = true;
  }

  @action
  saveInstructors(newInstructors, newInstructorGroups) {
    this.args.learnerGroup.set('instructors', newInstructors.slice());
    this.args.learnerGroup.set('instructorGroups', newInstructorGroups.slice());
    return this.args.learnerGroup.save();
  }

  addUserToGroup = enqueueTask(async (user) => {
    const learnerGroup = this.args.learnerGroup;
    const topLevelGroup = await learnerGroup.topLevelGroup;
    const removeGroups = await topLevelGroup.removeUserFromGroupAndAllDescendants(user);
    const addGroups = await learnerGroup.addUserToGroupAndAllParents(user);
    await Promise.all(removeGroups.map((g) => g.save()));
    await Promise.all(addGroups.map((g) => g.save()));
    this.usersToPassToManager = await this.createUsersToPassToManager.perform();
    this.usersToPassToCohortManager = await this.createUsersToPassToCohortManager.perform();
  });

  removeUserToCohort = enqueueTask(async (user) => {
    const topLevelGroup = await this.args.learnerGroup.topLevelGroup;
    const groups = await topLevelGroup.removeUserFromGroupAndAllDescendants(user);
    await all(groups.map((group) => group.save()));
    this.usersToPassToManager = await this.createUsersToPassToManager.perform();
    this.usersToPassToCohortManager = await this.createUsersToPassToCohortManager.perform();
  });

  addUsersToGroup = enqueueTask(async (users) => {
    const learnerGroup = this.args.learnerGroup;
    const topLevelGroup = await learnerGroup.topLevelGroup;
    let addGroups = [];
    let removeGroups = [];
    for (let i = 0; i < users.length; i++) {
      const user = users[i];
      removeGroups = [
        ...removeGroups,
        ...(await topLevelGroup.removeUserFromGroupAndAllDescendants(user)),
      ];
      addGroups = [...addGroups, ...(await learnerGroup.addUserToGroupAndAllParents(user))];
    }

    await Promise.all(uniqueValues(removeGroups).map((g) => g.save()));
    await Promise.all(uniqueValues(addGroups).map((g) => g.save()));

    this.usersToPassToManager = await this.createUsersToPassToManager.perform();
    this.usersToPassToCohortManager = await this.createUsersToPassToCohortManager.perform();
  });

  removeUsersToCohort = enqueueTask(async (users) => {
    const topLevelGroup = await this.args.learnerGroup.topLevelGroup;
    let groupsToSave = [];
    for (let i = 0; i < users.length; i++) {
      const user = users[i];
      const removeGroups = await topLevelGroup.removeUserFromGroupAndAllDescendants(user);
      groupsToSave = [...groupsToSave, ...removeGroups];
    }
    await all(uniqueValues(groupsToSave).map((group) => group.save()));
    this.usersToPassToManager = await this.createUsersToPassToManager.perform();
    this.usersToPassToCohortManager = await this.createUsersToPassToCohortManager.perform();
  });

  createUsersToPassToManager = task(async () => {
    let users;
    if (this.args.isEditing) {
      const topLevelGroup = await this.args.learnerGroup.getTopLevelGroup();
      users = await topLevelGroup.getAllDescendantUsers();
    } else {
      users = await this.args.learnerGroup.getUsersOnlyAtThisLevel();
    }
    return await map(users.slice(), async (user) => {
      const lowestGroupInTree = await user.getLowestMemberGroupInALearnerGroupTree(this.treeGroups);
      return ObjectProxy.create({
        content: user,
        lowestGroupInTree,
        //special sorting property
        lowestGroupInTreeTitle: lowestGroupInTree.title,
      });
    });
  });

  createUsersToPassToCohortManager = task(async () => {
    const learnerGroup = this.args.learnerGroup;
    const cohort = await learnerGroup.cohort;
    const topLevelGroup = await learnerGroup.getTopLevelGroup();
    const currentUsers = await topLevelGroup.getAllDescendantUsers();
    const users = await cohort.users;
    return users.filter((user) => !currentUsers.includes(user));
  });

  changeNeedsAccommodation = restartableTask(async (value) => {
    this.args.learnerGroup.set('needsAccommodation', value);
    await this.args.learnerGroup.save();
  });

  copyGroup = dropTask(async (withLearners, learnerGroup) => {
    const cohort = await learnerGroup.cohort;
    const parentGroup = await learnerGroup.parent;
    const newGroups = await cloneLearnerGroup(
      this.store,
      learnerGroup,
      cohort,
      withLearners,
      parentGroup,
    );
    // indicate that the top group is a copy
    newGroups[0].title = newGroups[0].title + ` (${this.intl.t('general.copy')})`;
    this.totalGroupsToSave = newGroups.length;
    // save groups one at a time because we need to save in this order so parents are saved before children
    for (let i = 0; i < newGroups.length; i++) {
      await newGroups[i].save();
      this.currentGroupsSaved = i + 1;
    }
    this.savedGroup = newGroups[0];
  });

  async getCoursesForGroupWithSubgroupName(prefix, learnerGroup) {
    const offerings = (await learnerGroup.offerings).slice();
    const ilms = (await learnerGroup.ilmSessions).slice();
    const arr = [].concat(offerings, ilms);
    const sessions = await Promise.all(mapBy(arr, 'session'));
    const filteredSessions = uniqueValues(sessions.filter(Boolean));
    const courses = await Promise.all(mapBy(filteredSessions, 'course'));
    const courseObjects = courses.map((course) => {
      const obj = {
        id: course.id,
        courseTitle: course.title,
        groups: [],
        course,
      };
      if (prefix) {
        obj.groups.push(`${prefix}>${learnerGroup.title}`);
      }
      return obj;
    });
    const children = (await learnerGroup.children).slice();
    const childCourses = await map(children, async (child) => {
      return await this.getCoursesForGroupWithSubgroupName(learnerGroup.title, child);
    });
    const comb = [...courseObjects, ...childCourses.flat()];
    return comb.reduce((arr, obj) => {
      let courseObj = findById(arr, obj.id);
      if (!courseObj) {
        courseObj = {
          id: obj.id,
          courseTitle: obj.courseTitle,
          groups: [],
          course: obj.course,
        };
        arr.push(courseObj);
      }
      courseObj.groups = [...courseObj.groups, ...obj.groups];
      uniqueValues(courseObj.groups);
      return arr;
    }, []);
  }
}