jccguimaraes/project-viewer-plus

View on GitHub
lib/services/state.js

Summary

Maintainability
A
1 hr
Test Coverage
import uuid from 'uuid';
import { Emitter } from 'atom';

import { DOCK_URI } from '../constants/base';

/* eslint-disable-next-line require-jsdoc */
class State {
  /* eslint-disable-next-line require-jsdoc */
  activate (currentState) {
    this.mapping = new Map();
    this.emitter = new Emitter();
  }

  /**
   * Clears the mapping
   */
  deactivate () {
    this.emitter.dispose();
    this.mapping.clear();
  }

  /**
   * Clears the mapping
   */
  clearState () {
    this.mapping.clear();
    this.emitter.emit('did-change-state');
  }

  /* eslint-disable-next-line require-jsdoc */
  createGroup (entry = {}, childrenIds = [], parentId = NaN) {
    return {
      type: 'group',
      name: entry.name || '',
      icon: entry.icon || '',
      order: entry.order || 'alphabetically',
      folding: entry.folding || 'collapsed',
      childrenIds,
      parentId
    };
  }

  /* eslint-disable-next-line require-jsdoc */
  createProject (entry = {}, parentId = NaN) {
    return {
      type: 'project',
      name: entry.name || '',
      icon: entry.icon || '',
      paths: entry.paths || [],
      parentId
    };
  }

  /**
   * Stores in the Map an object with valid group content. In case of the root
   * level, the object is stored with the "id" NaN.
   * @param {Object} entry - an object already validated with group content
   * @param {boolean} isRoot - indicates if this level is the root
   * @param {boolean} parentId - indicates if this level is the root
   * @returns {number} the id of the deserialized group
   */
  deserializeGroupAndReturnId (entry, isRoot, parentId) {
    const id = isRoot ? NaN : entry.id || uuid();
    const childrenIds = [];

    entry.groups.forEach(group =>
      childrenIds.push(this.deserializeGroupAndReturnId(group, false, id))
    );

    entry.projects.forEach(project =>
      childrenIds.push(this.deserializeProjectAndReturnId(project, id))
    );

    const group = this.createGroup(
      entry,
      childrenIds,
      isRoot ? undefined : parentId
    );

    this.mapping.set(id, group);

    return id;
  }

  /**
   * Stores in the Map an object with valid group content.
   * @param {Object} entry - an object already validated with project content
   * @param {Object} parentId - an object already validated with project content
   * @returns {number} the id of the deserialized project
   */
  deserializeProjectAndReturnId (entry, parentId) {
    const id = entry.id || uuid();
    const project = this.createProject(entry, parentId);

    this.mapping.set(id, project);

    return id;
  }

  /**
   * Parse state to store in cache or file
   * @param {number} id - the current id of the group (NaN for root)
   * @param {boolean} withContext - false for saving to file
   * @returns {Object} the serialized state
   */
  serializeGroupById (id, withContext = true) {
    const level = { groups: [], projects: [] };

    const group = this.getEntry(id || NaN);

    if (!group) {
      return level;
    }

    group.childrenIds.forEach(childId => {
      const entry = this.getEntry(childId);

      if (entry.type === 'group') {
        const serializedLevel = this.serializeGroupById(childId, withContext);

        level.groups.push({
          ...withContext ? { id: childId } : {},
          ...withContext ? { parentId: id } : {},
          name: entry.name,
          icon: entry.icon,
          order: entry.order,
          ...withContext ? { folding: entry.folding } : {},
          ...serializedLevel
        });
      }
      else if (entry.type === 'project') {
        level.projects.push({
          ...withContext ? { id: childId } : {},
          ...withContext ? { parentId: id } : {},
          name: entry.name,
          icon: entry.icon,
          paths: entry.paths
        });
      }
    });

    return level;
  }

  /**
   * Update parcially or fully an existing entry
   * @param {string} id - the id of the existing entry
   * @param {string} state - the new state (partial parameters or all of them)
   */
  fullOrParcialUpdateExistingEntry (id, state) {
    const entry = this.getEntry(id);

    if (!entry) {
      throw new Error('unexisting_entry');
    }

    this.mapping.set(id, {
      ...entry,
      ...state
    });
  }

  /* eslint-disable-next-line require-jsdoc */
  getEntry (id) {
    return this.mapping.get(id);
  }

  /* eslint-disable-next-line require-jsdoc */
  deleteEntry (id) {
    const entry = this.getEntry(id);

    if (entry.type === 'group') {
      entry.childrenIds.forEach(childId => this.deleteEntry(childId));
    }

    if (entry.parentId || isNaN(entry.parentId)) {
      const group = this.getEntry(entry.parentId);
      const idx = group.childrenIds.indexOf(id);
      group.childrenIds.splice(idx, 1);
      this.fullOrParcialUpdateExistingEntry(entry.parentId, group);
    }

    this.mapping.delete(id);
    this.emitter.emit('did-change-state');
  }

  /**
   * Given an group id it will search all projects underneath it
   * @param {number} groupId - the group id to search for projects
   * @param {array} list - the container for all projects
   * @returns {array} the container for all projects
   */
  getProjectsInGroup (groupId, list = []) {
    if (this.mapping.size === 0) {
      return list;
    }

    const group = this.getEntry(groupId || NaN);

    if (!group || group.type !== 'group') {
      throw new Error('not_a_group');
    }

    return group.childrenIds.reduce((acc, entryId) => {
      const entry = this.getEntry(entryId);
      if (entry.type === 'group') {
        this.getProjectsInGroup(entryId, acc);
      }
      else {
        acc.push({
          id: entryId,
          name: entry.name,
          paths: entry.paths
        });
      }

      return acc;
    }, list);
  }

  /**
   * Given an group id it will search all groups underneath it
   * @param {number} groupId - the group id to search for groups
   * @param {array} list - the container for all groups
   * @returns {array} the container for all groups
   */
  getGroupsInGroup (groupId, list = []) {
    const group = this.getEntry(groupId || NaN);

    if (!group || group.type !== 'group') {
      throw new Error('not_a_group');
    }

    return group.childrenIds.reduce((acc, entryId) => {
      const entry = this.getEntry(entryId);
      if (entry.type === 'group') {
        const subList = [];

        acc.push({
          id: entryId,
          name: entry.name,
          groups: this.getGroupsInGroup (entryId, subList)
        });
      }

      return acc;
    }, list);
  }

  /* eslint-disable-next-line require-jsdoc */
  editEntry (id, newState) {
    const entry = this.getEntry(id);

    if (entry.parentId !== newState.parentId) {
      const oldParent = this.getEntry(entry.parentId);
      const newParent = this.getEntry(newState.parentId);

      oldParent.childrenIds.splice(oldParent.childrenIds.indexOf(id), 1);
      newParent.childrenIds.push(id);
    }

    this.fullOrParcialUpdateExistingEntry(id, newState);
    this.emitter.emit('did-change-state');
  }

  /* eslint-disable-next-line require-jsdoc */
  addEntry (entry) {
    if (!entry) {
      return;
    }

    const id = uuid();

    this.mapping.set(id, entry);
    const parent = this.getEntry(entry.parentId);

    if (parent) {
      parent.childrenIds.push(id);
    }

    this.fullOrParcialUpdateExistingEntry(entry.parentId, parent);

    this.emitter.emit('did-change-state');
  }

  /* eslint-disable-next-line require-jsdoc */
  setParentOfEntry (entryId, parentId = NaN) {
    if (entryId === parentId || entryId === DOCK_URI) {
      return;
    }
    const entry = this.getEntry(entryId);

    if (!entry) {
      return;
    }

    const oldParent = this.getEntry(entry.parentId);

    let newParent = this.getEntry(parentId);

    if (newParent.type === 'project') {
      parentId = newParent.parentId;
      newParent = this.getEntry(newParent.parentId);
    }

    oldParent.childrenIds.splice(oldParent.childrenIds.indexOf(entryId), 1);

    newParent.childrenIds.push(entryId);

    entry.parentId = parentId;

    this.fullOrParcialUpdateExistingEntry(entryId, entry);
    this.fullOrParcialUpdateExistingEntry(entry.parentId, oldParent);
    this.fullOrParcialUpdateExistingEntry(parentId, newParent);

    this.emitter.emit('did-change-state');
  }

  /* eslint-disable-next-line require-jsdoc */
  initializeState (currentState) {
    this.deserializeGroupAndReturnId(currentState, true);
    this.emitter.emit('did-change-state');
  }

  /* eslint-disable-next-line require-jsdoc */
  onDidChangeState (cb) {
    this.emitter.on('did-change-state', cb);
  }
}

// same instance is shared across the package
export default new State();