Codeminer42/cm42-central

View on GitHub
app/assets/javascripts/components/projects/ProjectBoard.jsx

Summary

Maintainability
B
4 hrs
Test Coverage
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import {
  fetchProjectBoard,
  toggleColumn,
  reverseColumns,
} from 'actions/projectBoard';
import { fetchPastStories } from 'actions/pastIterations';
import { Column } from '../Columns/ColumnItem';
import History from '../stories/History';
import { getColumns } from '../../selectors/columns';
import { closeHistory, createStory, dragDropStory } from '../../actions/story';
import { CHILLY_BIN, DONE, BACKLOG, EPIC } from '../../models/beta/column';
import PropTypes from 'prop-types';
import {
  canCloseColumn,
  getPositions,
  getNewSprints,
  getNewState,
  moveStory,
  getSprintColumn,
  dragStory,
} from '../../models/beta/projectBoard';
import { historyStatus, columns, storyTypes } from 'libs/beta/constants';
import StoryPropTypes from '../shapes/story';
import ProjectBoardPropTypes from '../shapes/projectBoard';
import Notifications from '../Notifications';
import { removeNotification } from '../../actions/notifications';
import StorySearch from '../search/StorySearch';
import SprintVelocitySimulation from '../sprint/SprintVelocitySimulation';
import SearchResults from './../search/SearchResults';
import ProjectLoading from './ProjectLoading';
import SideBar from './SideBar';
import Columns from '../Columns';
import EpicColumn from '../Columns/EpicColumn';
import { DragDropContext } from 'react-beautiful-dnd';
import { subscribeToProjectChanges } from '../../pusherSockets';

export const ProjectBoard = ({
  fetchProjectBoard,
  projectId,
  projectBoard,
  closeHistory,
  notifications,
  removeNotification,
  history,
  chillyBinStories,
  backlogSprints,
  toggleColumn,
  reverseColumns,
  doneSprints,
  createStory,
  dragDropStory,
  fetchPastStories,
  epicStories,
}) => {
  const [newChillyBinStories, setNewChillyBinStories] = useState([]);
  const [newBacklogSprints, setNewBacklogSprints] = useState([]);

  useEffect(() => {
    setNewBacklogSprints(backlogSprints);
  }, [setNewBacklogSprints, backlogSprints]);

  useEffect(() => {
    setNewChillyBinStories(chillyBinStories);
  }, [setNewChillyBinStories, chillyBinStories]);

  useEffect(() => {
    fetchProjectBoard(projectId);
  }, [projectId, fetchProjectBoard]);

  useEffect(() => {
    const project = { id: projectId };
    const unsubscribe = subscribeToProjectChanges(project, () => {
      fetchProjectBoard(projectId);
    });
  }, [projectId, fetchProjectBoard]);

  if (!projectBoard.isFetched && projectBoard.isInitialLoading) {
    return <ProjectLoading data-id="project-loading" />;
  }

  const onDragEnd = ({ source, destination }) => {
    if (!destination || !source) return;

    const { sprintIndex: sprintDropIndex, columnId: dropColumn } = JSON.parse(
      destination.droppableId
    );
    const { sprintIndex: sprintDragIndex, columnId: dragColumn } = JSON.parse(
      source.droppableId
    );
    const { index: sourceIndex } = source;
    const { index: destinationIndex } = destination;
    const isSameColumn = dragColumn === dropColumn;
    const destinationArray = getSprintColumn(
      dropColumn,
      newBacklogSprints,
      newChillyBinStories,
      sprintDropIndex
    ); // stories of destination column
    const sourceArray = getSprintColumn(
      dragColumn,
      newBacklogSprints,
      newChillyBinStories,
      sprintDragIndex
    ); // stories of source column
    const dragStory = sourceArray[sourceIndex];

    if (isSameColumn && sourceIndex === destinationIndex) return;
    if (!dropColumn) return;
    if (
      !isSameColumn &&
      dragStory.storyType === storyTypes.FEATURE &&
      !dragStory.estimate
    )
      return;

    const [position, newPosition] = getPositions(
      destinationIndex,
      sourceIndex,
      destinationArray,
      isSameColumn,
      dragStory.state
    );

    const newStories = moveStory(
      sourceArray,
      destinationArray,
      sourceIndex,
      destinationIndex,
      isSameColumn
    );

    // Changing the column array order
    if (dropColumn === columns.CHILLY_BIN) {
      setNewChillyBinStories(newStories);
    }

    if (dropColumn === columns.BACKLOG) {
      setNewBacklogSprints(
        getNewSprints(newStories, newBacklogSprints, sprintDropIndex)
      );
    }

    // Persisting the new array order
    const newState = getNewState(isSameColumn, dropColumn, dragStory.state);

    return dragDropStory(dragStory.id, dragStory.projectId, {
      position,
      newPosition,
      state: newState,
    });
  };

  const onDragUpdate = ({ source, destination }) => {
    dragStory(source, destination, newBacklogSprints, setNewBacklogSprints);
  };

  return (
    <DragDropContext onDragEnd={onDragEnd} onDragUpdate={onDragUpdate}>
      <div className="ProjectBoard">
        <SprintVelocitySimulation />

        <StorySearch
          projectId={projectId}
          loading={projectBoard.search.loading}
        />

        <SideBar
          data-id="side-bar"
          reverse={projectBoard.reverse}
          visibleColumns={projectBoard.visibleColumns}
          toggleColumn={toggleColumn}
          reverseColumns={reverseColumns}
        />

        <Notifications
          notifications={notifications}
          onRemove={removeNotification}
          data-id="notifications"
        />

        <Columns
          canClose={canCloseColumn(projectBoard)}
          chillyBinStories={newChillyBinStories}
          backlogSprints={newBacklogSprints}
          doneSprints={doneSprints}
          toggleColumn={toggleColumn}
          visibleColumns={projectBoard.visibleColumns}
          createStory={createStory}
          fetchPastStories={fetchPastStories}
          reverse={projectBoard.reverse}
          data-id="columns"
        />

        <SearchResults />

        {epicStories.length && (
          <EpicColumn stories={epicStories} data-id="epic-column" />
        )}

        {history.status !== historyStatus.DISABLED && (
          <Column
            onClose={closeHistory}
            title={`${I18n.t('projects.show.history')} '${history.storyTitle}'`}
            data-id="history-column"
            canClose
          >
            {history.status === historyStatus.LOADED ? (
              <History history={history.activities} data-id="history" />
            ) : (
              <ProjectLoading data-id="project-loading" />
            )}
          </Column>
        )}
      </div>
    </DragDropContext>
  );
};

ProjectBoard.propTypes = {
  projectBoard: ProjectBoardPropTypes.isRequired,
  chillyBinStories: PropTypes.arrayOf(StoryPropTypes),
  doneSprints: PropTypes.array.isRequired,
  backlogSprints: PropTypes.array.isRequired,
  fetchProjectBoard: PropTypes.func.isRequired,
  closeHistory: PropTypes.func.isRequired,
  notifications: PropTypes.array.isRequired,
  reverseColumns: PropTypes.func.isRequired,
  projectId: PropTypes.string.isRequired,
  removeNotification: PropTypes.func.isRequired,
  history: PropTypes.object.isRequired,
  fetchPastStories: PropTypes.func.isRequired,
  toggleColumn: PropTypes.func.isRequired,
  calculatedSprintVelocity: PropTypes.number,
  sprintVelocity: PropTypes.number,
  simulateSprintVelocity: PropTypes.func,
  revertSprintVelocity: PropTypes.func,
  createStory: PropTypes.func.isRequired,
};

const mapStateToProps = ({
  projectBoard,
  project,
  stories,
  history,
  pastIterations,
  notifications,
}) => ({
  projectBoard,
  history,
  chillyBinStories: getColumns({
    column: CHILLY_BIN,
    stories,
  }),
  backlogSprints: getColumns({
    column: BACKLOG,
    stories,
    project,
    pastIterations,
  }),
  doneSprints: getColumns({
    column: DONE,
    pastIterations,
    stories,
  }),
  epicStories: getColumns({
    column: EPIC,
    stories,
  }),
  notifications,
});

const mapDispatchToProps = {
  fetchProjectBoard,
  toggleColumn,
  closeHistory,
  fetchPastStories,
  removeNotification,
  reverseColumns,
  dragDropStory,
  createStory,
};

export default connect(mapStateToProps, mapDispatchToProps)(ProjectBoard);