rrebase/knboard

View on GitHub
frontend/src/features/board/Board.tsx

Summary

Maintainability
A
3 hrs
Test Coverage
import React from "react";
import styled from "@emotion/styled";
import {
  DragDropContext,
  Droppable,
  DroppableProvided,
  DraggableLocation,
  DropResult,
} from "react-beautiful-dnd";
import Column from "features/column";
import { IColumn } from "types";
import reorder, { reorderTasks } from "utils/reorder";
import { RootState } from "store";
import { useSelector, useDispatch } from "react-redux";
import { updateTasksByColumn } from "features/task/TaskSlice";
import { updateColumns, columnSelectors } from "features/column/ColumnSlice";
import { useParams } from "react-router-dom";
import { fetchBoardById } from "./BoardSlice";
import Spinner from "components/Spinner";
import { barHeight, sidebarWidth } from "const";
import PageError from "components/PageError";
import SEO from "components/SEO";

const BoardContainer = styled.div`
  min-width: calc(100vw - ${sidebarWidth});
  min-height: calc(100vh - ${barHeight * 2}px);
  overflow-x: scroll;
  display: flex;
`;

const ColumnContainer = styled.div`
  display: inline-flex;
  width: 100%;
`;

const EmptyBoard = styled.div`
  display: flex;
  justify-content: center;
  margin-top: 50px;
`;

const ColumnsBlock = styled.div``;

const RightMargin = styled.div`
  /* 
  With overflow-x the right-margin of the rightmost column is hidden.
  This is a dummy element that fills up the space to make it 
  seem like there's some right margin.
   */
  &:after {
    content: "";
    display: block;
    width: 0.5rem;
  }
`;

const Board = () => {
  const detail = useSelector((state: RootState) => state.board.detail);
  const error = useSelector((state: RootState) => state.board.detailError);
  const columns = useSelector(columnSelectors.selectAll);
  const tasksByColumn = useSelector((state: RootState) => state.task.byColumn);
  const tasksById = useSelector((state: RootState) => state.task.byId);
  const dispatch = useDispatch();
  const { id } = useParams();

  React.useEffect(() => {
    if (id) {
      dispatch(fetchBoardById({ boardId: id }));
    }
  }, [id]);

  const onDragEnd = (result: DropResult) => {
    // dropped nowhere
    if (!result.destination) {
      return;
    }

    const source: DraggableLocation = result.source;
    const destination: DraggableLocation = result.destination;

    // did not move anywhere - can bail early
    if (
      source.droppableId === destination.droppableId &&
      source.index === destination.index
    ) {
      return;
    }

    // reordering column
    if (result.type === "COLUMN") {
      const newOrdered: IColumn[] = reorder(
        columns,
        source.index,
        destination.index
      );
      dispatch(updateColumns(newOrdered));
      return;
    }

    const data = reorderTasks({
      tasksByColumn,
      source,
      destination,
    });
    dispatch(updateTasksByColumn(data.tasksByColumn));
  };

  const detailDataExists = detail?.id.toString() === id;

  if (error) {
    return <PageError>{error}</PageError>;
  }

  if (!detailDataExists) {
    return <Spinner loading={!detailDataExists} />;
  }

  return (
    <>
      <SEO title={detail?.name} />
      {columns.length !== 0 ? (
        <BoardContainer data-testid="board-container">
          <ColumnsBlock>
            <DragDropContext onDragEnd={onDragEnd}>
              <Droppable
                droppableId="board"
                type="COLUMN"
                direction="horizontal"
              >
                {(provided: DroppableProvided) => (
                  <ColumnContainer
                    ref={provided.innerRef}
                    {...provided.droppableProps}
                  >
                    {columns.map((column: IColumn, index: number) => (
                      <Column
                        key={column.id}
                        id={column.id}
                        title={column.title}
                        index={index}
                        tasks={tasksByColumn[column.id].map(
                          (taskId) => tasksById[taskId]
                        )}
                      />
                    ))}
                    {provided.placeholder}
                  </ColumnContainer>
                )}
              </Droppable>
            </DragDropContext>
          </ColumnsBlock>
          <RightMargin />
        </BoardContainer>
      ) : (
        <EmptyBoard>This board is empty.</EmptyBoard>
      )}
    </>
  );
};

export default Board;