wongjiahau/ttap-web

View on GitHub
src/ts/react/timetableView/timetableView.tsx

Summary

Maintainability
A
3 hrs
Test Coverage
import Button from "@material-ui/core/Button";
import * as React from "react";
import { TimePeriod } from "../../att/timePeriod";
import { STCBox } from "../../model/matrix/stcBox";
import { ISlotViewModel } from "../../model/slotViewModel";
import * as ReactGridLayout from "../../modified_node_modules/react-grid-layout";
import { Colors } from "../colors/colors";
import { StackPanel } from "../panels/stackPanel";
import { TimetableSummaryView } from "../timetableSummaryView";
import { GenerateSlotViewsAndDayColumn } from "./generateSlotViewsAndDayColumn";
import { GenerateStateViews } from "./generateStateView";
import { ISkeleton, Skeleton } from "./skeleton";
import { ArcherContainer } from "react-archer";

const getTimetableViewWidth = () => 0.9 * window.innerWidth;

interface ITimetableViewProps {
  slots: ISlotViewModel[];
  stcBoxes: STCBox[] | null;
  alternateSlots: ISlotViewModel[] | null;
  isShowingAlternativeSlots: boolean;
  isShowingAlternativeSlotOf: ISlotViewModel | null;
  handleSetTimeContraintAt: (state: STCBox) => void;
  handleDesetTimeContraintAt: (state: STCBox) => void;
  handleToggleIsOpenOfSummary: () => void;
  handleSelectSlotChoice: (slotUid: number, newSlotChoice: number) => void;
  handleShowAlternateSlot: (s: ISlotViewModel) => void;
  handleGoToThisAlternateSlot: (
    sourceSloutUid: number,
    destinationSlotUid: number
  ) => void;
  isSummaryOpen?: boolean;
}

interface ITimetableViewState {
  width: number;
}

/* This component have two purpose
 *  1. To render TimetableView
 *  2. To render SetTimeConstraintView
 */
export class TimetableView extends React.Component<
  ITimetableViewProps,
  ITimetableViewState
> {
  public constructor(props: ITimetableViewProps) {
    super(props);
    this.state = {
      width: getTimetableViewWidth(),
    };
  }
  public render() {
    const skeleton = new Skeleton();
    if (this.props.slots && this.props.alternateSlots) {
      // render timetable view
      const slotViewsAndDayColumn = GenerateSlotViewsAndDayColumn(
        this.props.slots.concat(this.props.alternateSlots),
        this.props.handleSelectSlotChoice,
        this.props.handleGoToThisAlternateSlot,
        this.props.handleShowAlternateSlot,
        this.props.isShowingAlternativeSlotOf
      );
      skeleton.Concat(slotViewsAndDayColumn);
      const horizontalDividers = GenerateHorizontalDividers(skeleton);
      skeleton.Concat(horizontalDividers);
    }
    if (this.props.stcBoxes) {
      // render set time constraint view
      const stateViews = GenerateStateViews(
        this.props.stcBoxes,
        this.props.handleSetTimeContraintAt,
        this.props.handleDesetTimeContraintAt
      );
      skeleton.Concat(stateViews);
      skeleton.Layouts = skeleton.Layouts.concat(GetStandardDayColumnLayout());
    }
    const divStyle: React.CSSProperties = {
      backgroundColor: Colors.WhiteSmoke,
      borderStyle: "solid",
      borderWidth: "1px",
      borderRadius: "5px",
      fontFamily: "roboto",
      margin: "auto",
      position: "relative",
      width: this.state.width,
    };
    const buttonStyle: React.CSSProperties = {
      bottom: "0",
      fontSize: "12px",
      position: "absolute",
      right: "0",
    };
    return (
      <div id="timetable-view" style={{ padding: "12px 0", display: "grid" }}>
        {/* Tippy css */} <link rel="stylesheet" href="tippy.css" />
        <div style={{ display: "grid", gridGap: "12px" }}>
          <div style={divStyle}>
            <ArcherContainer
              strokeColor="red"
              arrowLength={0}
              arrowThickness={0}
            >
              <ReactGridLayout
                cols={(TimePeriod.Max.Hour - TimePeriod.Min.Hour) * 2 + 2}
                maxRows={50}
                rowHeight={50}
                width={this.state.width}
                layout={skeleton.Layouts}
                margin={[0, 0]}
                isDraggable={false}
                isResizable={false}
                autoSize={true}
                verticalCompact={false}
              >
                {skeleton.Children}
              </ReactGridLayout>
            </ArcherContainer>
            {this.props.slots.length > 0 ? (
              <Button
                id="summary-btn"
                variant="contained"
                style={buttonStyle}
                onClick={this.props.handleToggleIsOpenOfSummary}
              >
                {this.props.isSummaryOpen ? "hide summary" : "show summary"}
              </Button>
            ) : null}
          </div>
          {this.props.isSummaryOpen && (
            <TimetableSummaryView slots={this.props.slots} />
          )}
        </div>
      </div>
    );
  }
  public componentDidMount() {
    this.previousOnResizeHandler = window.onresize;
    window.onresize = this.handleWindowResizing;
  }

  public componentWillUnmount() {
    // This is needed to fix issue #133
    // Refer https://github.com/wongjiahau/ttap-web/issues/133
    window.onresize = this.previousOnResizeHandler;
  }

  public handleWindowResizing = () => {
    this.setState({ width: getTimetableViewWidth() });
  };

  private previousOnResizeHandler: any = () => {};
}

export const GetStandardDayColumnLayout = (): ReactGridLayout.Layout[] => {
  const result = Array<ReactGridLayout.Layout>();
  const NUMBER_OF_DAY_PER_WEEK = 7;
  for (let j = 0; j <= NUMBER_OF_DAY_PER_WEEK; j++) {
    result.push({
      h: 1,
      i: "d" + j,
      w: 2,
      x: 0,
      y: j,
    });
  }
  return result;
};

export const GenerateHorizontalDividers = (skeleton: ISkeleton): ISkeleton => {
  const getDivider = (layoutId: string) => {
    const dividerStyle: React.CSSProperties = {
      borderBottom: "1px dotted #666",
      width: "100%",
    };
    return <div key={layoutId} style={dividerStyle} />;
  };
  const dividers: JSX.Element[] = [];
  for (let i = 1; i <= 6; i++) {
    dividers.push(getDivider("divider" + i));
  }
  const dividersLayouts: ReactGridLayout.Layout[] = [];
  for (let i = 1; i <= 6; i++) {
    dividersLayouts.push({
      ...skeleton.Layouts.filter((x) => x.i === "d" + i)[0],
      i: "divider" + i,
      w: (TimePeriod.Max.Hour - TimePeriod.Min.Hour) * 2,
      x: 2,
    });
  }
  return {
    Children: dividers,
    Layouts: dividersLayouts,
  };
};

/*
Note: For the horizontal borders to work, the synchronizeLayoutWithChildren function of ReactGirdLayout must be disabled,
It can be disabled by returning the initialLayout directly
in utils.js of ReactGridLayout folder
*/