hexlet-codebattle/codebattle

View on GitHub
services/app/apps/codebattle/assets/js/widgets/pages/builder/BuilderEditorsWidget.jsx

Summary

Maintainability
D
2 days
Test Coverage
import React, {
  useState,
  useContext,
  useCallback,
  memo,
} from 'react';

import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import cn from 'classnames';
import noop from 'lodash/noop';
import { useDispatch, useSelector } from 'react-redux';

import Editor from '../../components/Editor';
import LanguagePickerView from '../../components/LanguagePickerView';
import RoomContext from '../../components/RoomContext';
import { gameRoomEditorStyles } from '../../config/editorSettings';
import assertsStatuses from '../../config/executionStatuses';
import {
  isTaskAssertsFormingSelector,
  isTaskAssertsReadySelector,
  taskStateSelector,
} from '../../machines/selectors';
import {
  getGeneratorStatus,
  validationStatuses,
} from '../../machines/task';
import { reloadGeneratorAndSolutionTemplates } from '../../middlewares/Room';
import * as selectors from '../../selectors';
import { actions } from '../../slices';
import { taskTemplatesStates } from '../../utils/builder';
import useMachineStateSelector from '../../utils/useMachineStateSelector';
import DakModeButton from '../game/DarkModeButton';
import VimModeButton from '../game/VimModeButton';

import AssertsOutput from './AssertsOutput';
import TaskPropStatusIcon from './TaskPropStatusIcon';

const isGeneratorsError = status => (
  status === assertsStatuses.error
    || status === assertsStatuses.memoryLeak
    || status === assertsStatuses.timeout
);

const InfoPopup = ({ reloadGeneratorCode, editable, origin }) => {
  const infoClassName = cn(
    'd-flex align-items-center justify-content-around position-absolute w-100 h-100 p-3',
    'bg-gray text-black  cb-opacity-75',
  );
  if (origin === 'github') {
    return (
      <div className={infoClassName}>
        <span className="text-center">Asserts are pre-generated</span>
      </div>
    );
  }

  return (
    <div className={infoClassName}>
      {editable ? (
        <span className="text-center">
          Reload (Press
          {' '}
          <button
            type="button"
            className="btn border-0 rounded-lg p-1"
            onClick={reloadGeneratorCode}
          >
            <FontAwesomeIcon icon="redo" />
          </button>
          ) Generator/Solution code.
        </span>
      ) : (
        <span className="text-center">Tests are not generated automatically. They are based only on examples</span>
      )}
    </div>
  );
};

function BuilderEditorsWidget() {
  const dispatch = useDispatch();
  const { taskService } = useContext(RoomContext);

  const [assertsPanelShowing, setAssertsPanelShowing] = useState(false);

  const taskMachineState = useMachineStateSelector(taskService, taskStateSelector);
  const isAssertsReady = isTaskAssertsReadySelector(taskMachineState);
  const isAssertsForming = isTaskAssertsFormingSelector(taskMachineState);

  const editable = useSelector(selectors.canEditTaskGenerator);
  const templatesState = useSelector(selectors.taskTemplatesStateSelector);
  const origin = useSelector(selectors.taskOriginSelector);
  const asserts = useSelector(selectors.taskAssertsSelector);
  const assertsStatus = useSelector(selectors.taskAssertsStatusSelector);
  const editorsLang = useSelector(selectors.taskGeneratorLangSelector);
  const textArgumentsGenerator = useSelector(
    selectors.taskTextArgumentsGeneratorSelector,
  );
  const textSolution = useSelector(selectors.taskTextSolutionSelector);
  const currentUserId = useSelector(selectors.currentUserIdSelector);
  const theme = useSelector(selectors.editorsThemeSelector);
  const editorsMode = useSelector(selectors.editorsModeSelector);

  const [isValidArgumentsGenerator, invalidGeneratorReason] = useSelector(
    state => state.builder.validationStatuses.argumentsGenerator,
  );
  const [isValidSolution, invalidSolutionReason] = useSelector(
    state => state.builder.validationStatuses.solution,
  );

  const reloadCode = useCallback(() => {
    dispatch(reloadGeneratorAndSolutionTemplates(taskService));
  }, [taskService, dispatch]);

  const resetCode = useCallback(() => {
    dispatch(actions.resetGeneratorAndSolution());
    taskService.send('CHANGES');
  }, [dispatch, taskService]);

  const rejectAssertsGeneration = useCallback(() => {
    dispatch(actions.rejectGeneratorAndSolution());
    taskService.send('CHANGES');
  }, [dispatch, taskService]);

  const toggleAssertsPanel = useCallback(() => {
    setAssertsPanelShowing(!assertsPanelShowing);
  }, [setAssertsPanelShowing, assertsPanelShowing]);

  const disabledEditors = templatesState === taskTemplatesStates.none;
  const validationStarts = isAssertsForming;

  const params = {
    editable: editable && !disabledEditors && !validationStarts,
    syntax: editorsLang,
    mode: editorsMode,
    theme,
    mute: true,
    loading: false,
  };

  const changeTaskServiceState = useCallback(() => taskService.send('CHANGES'), [taskService]);
  const handleChanges = isAssertsReady
    ? changeTaskServiceState
    : noop;

  const onChangeGenerator = useCallback(value => {
    handleChanges();

    dispatch(actions.setTaskArgumentsGenerator({ value }));
  }, [dispatch, handleChanges]);
  const onChangeSolution = useCallback(value => {
    handleChanges();

    dispatch(actions.setTaskSolution({ value }));
  }, [dispatch, handleChanges]);

  const generatorParams = {
    ...params,
    value: textArgumentsGenerator,
    onChange: onChangeGenerator,
  };

  const solutionParams = {
    ...params,
    value: textSolution,
    onChange: onChangeSolution,
  };

  const assertsBadgeClassName = cn('badge ml-1', {
    'badge-light': assertsStatus.status === assertsStatuses.none,
    'badge-warning': assertsStatus.status === assertsStatuses.failure,
    'badge-danger': isGeneratorsError(assertsStatus.status),
    'badge-success': assertsStatus.status === assertsStatuses.success,
  });

  return (
    <>
      <div className="col-12 col-lg-6 p-1" data-editor-state="idle">
        <div
          className="card h-100 shadow-sm position-relative"
          style={gameRoomEditorStyles}
        >
          <div className="rounded-top" data-player-type="current_user">
            <div className="btn-toolbar justify-content-between align-items-center m-1 mb-2" role="toolbar">
              <div className="d-flex justify-content-between">
                <div className="d-flex align-items-center p-1">
                  <div className="py-2">
                    <TaskPropStatusIcon
                      id="editorArgumentsGenerator"
                      status={
                        !isValidArgumentsGenerator
                          ? validationStatuses.invalid
                          : getGeneratorStatus(templatesState, taskMachineState)
                      }
                      reason={invalidSolutionReason}
                    />
                  </div>
                  <h5 className="pt-2">Step 3: Generator</h5>
                </div>
                <div
                  className="btn-group align-items-center ml-2 mr-auto"
                  role="group"
                  aria-label="Editor mode"
                >
                  <VimModeButton playerId={currentUserId} />
                  <DakModeButton playerId={currentUserId} />
                </div>
              </div>
              <div
                className={
                  cn('btn-group justify-content-between align-items-center', {
                    'd-flex': params.edibatle,
                    'd-none': !params.editable,
                  })
                }
              >
                <button
                  title="Reset Code"
                  type="button"
                  className="btn btn-sm btn-light rounded-left"
                  onClick={resetCode}
                >
                  <FontAwesomeIcon className="mr-2" icon="sync" />
                  Reset
                </button>
                <button
                  title="Reject asserts generation"
                  type="button"
                  className="btn btn-sm btn-light ml-1 rounded-right"
                  onClick={rejectAssertsGeneration}
                >
                  <FontAwesomeIcon className="mr-2" icon="window-close" />
                  Reject
                </button>
              </div>
            </div>
          </div>
          <Editor {...generatorParams} />
          {disabledEditors && (
            <InfoPopup editable={editable} reloadGeneratorCode={reloadCode} origin={origin} />
          )}
        </div>
      </div>
      <div className="col-12 col-lg-6 p-1" data-editor-state="idle">
        <div className="card h-100 shadow-sm" style={gameRoomEditorStyles}>
          <div className="rounded-top" data-player-type="current_user">
            <div className="btn-toolbar justify-content-between align-items-center m-1" role="toolbar">
              <div className="d-flex align-items-center p-1">
                <div className="py-2">
                  <TaskPropStatusIcon
                    id="editorSolution"
                    status={
                      !isValidSolution
                        ? validationStatuses.invalid
                        : getGeneratorStatus(templatesState, taskMachineState)
                    }
                    reason={invalidGeneratorReason}
                  />
                </div>
                <h5 className="pt-2">Step 4: Solution Example</h5>
                <button
                  title={assertsPanelShowing ? 'Open solution code' : 'Open task asserts'}
                  type="button"
                  className="btn btn-secondary ml-2 rounded-lg text-nowrap"
                  onClick={toggleAssertsPanel}
                  disabled={assertsStatus.status === 'none'}
                >
                  {assertsPanelShowing ? (
                    <>
                      <FontAwesomeIcon className="mr-2" icon="edit" />
                      Code
                    </>
                  ) : (
                    <>
                      <FontAwesomeIcon className="mr-2" icon="tasks" />
                      Asserts
                      {asserts.length !== 0 && <span className={assertsBadgeClassName}>{asserts.length}</span>}
                      {asserts.length === 0 && isGeneratorsError(assertsStatus.status) && <span className={assertsBadgeClassName}>!</span>}
                    </>
                  )}
                </button>
              </div>
              <LanguagePickerView
                currentLangSlug={editorsLang}
                isDisabled
              />
            </div>
          </div>
          <div className={!assertsPanelShowing ? 'd-none' : ''}>
            <AssertsOutput asserts={asserts} {...assertsStatus} />
          </div>
          <div id="editor" className={assertsPanelShowing ? 'd-none' : 'd-flex flex-column flex-grow-1'}>
            <Editor {...solutionParams} />
          </div>
          {disabledEditors && (
            <InfoPopup editable={editable} reloadGeneratorCode={reloadCode} origin={origin} />
          )}
        </div>
      </div>
    </>
  );
}

export default memo(BuilderEditorsWidget);