FarmBot/Farmbot-Web-App

View on GitHub
frontend/sequences/panel/preview_support.tsx

Summary

Maintainability
B
4 hrs
Test Coverage
import React from "react";
import { t } from "../../i18next_wrapper";
import { EmptyStateWrapper, EmptyStateGraphic, Help, Markdown } from "../../ui";
import { isTaggedSequence } from "../../resources/tagged_resources";
import { Everything } from "../../interfaces";
import { SpecialStatus, TaggedSequence } from "farmbot";
import axios from "axios";
import { API } from "../../api";
import { useNavigate } from "react-router-dom";
import { noop } from "lodash";
import { ErrorBoundary } from "../../error_boundary";
import { AllSteps } from "../all_steps";
import { LocalsList } from "../locals_list/locals_list";
import { AllowedVariableNodes } from "../locals_list/locals_list_support";
import { ResourceIndex } from "../../resources/interfaces";
import { createSequenceMeta } from "../../resources/sequence_meta";
import { maybeTagStep } from "../../resources/sequence_tagging";
import { Content } from "../../constants";
import {
  getWebAppConfigValue, GetWebAppConfigValue,
} from "../../config_storage/actions";
import { stringifySequenceData } from "../step_tiles";
import { installSequence } from "../actions";
import { Collapse } from "@blueprintjs/core";
import {
  isSequencePublished, publishAction, SectionHeader,
} from "../sequence_editor_middle_active";
import moment from "moment";
import { edit } from "../../api/crud";
import { SequenceResource } from "farmbot/dist/resources/api_resources";
import { Path } from "../../internal_urls";
import { SequenceReducerState } from "../interfaces";

interface LoadSequenceVersionProps {
  id: string;
  onSuccess(sequence: TaggedSequence): void;
  onError(): void;
}

export const loadSequenceVersion = (props: LoadSequenceVersionProps) => {
  axios.get<SequenceResource>(API.current.sequenceVersionsPath + props.id)
    .then(response => {
      const sequence: TaggedSequence = {
        kind: "Sequence",
        uuid: "Sequence.0",
        specialStatus: SpecialStatus.SAVED,
        body: response.data,
      };
      sequence.body.name = sequence.body.name || `Shared Sequence ${props.id}`;
      sequence.body.body?.map(step => maybeTagStep(step));
      props.onSuccess(sequence);
    }, props.onError);
};

export interface SequencePreviewProps {
  dispatch: Function;
  resources: ResourceIndex;
  getWebAppConfigValue: GetWebAppConfigValue;
  sequencesState: SequenceReducerState;
}

export function mapStateToProps(props: Everything): SequencePreviewProps {
  return {
    dispatch: props.dispatch,
    resources: props.resources.index,
    getWebAppConfigValue: getWebAppConfigValue(() => props),
    sequencesState: props.resources.consumers.sequences,
  };
}

export interface SequencePreviewState {
  sequence: TaggedSequence | undefined;
  viewSequenceCeleryScript: boolean;
  variablesCollapsed: boolean;
  descriptionCollapsed: boolean;
  stepsCollapsed: boolean;
  licenseCollapsed: boolean;
  error: boolean;
}

interface SequencePreviewContentProps {
  sequence: TaggedSequence | undefined;
  error: boolean;
  viewCeleryScript: boolean;
  viewSequenceCeleryScript: boolean;
  dispatch: Function;
  resources: ResourceIndex;
  descriptionCollapsed: boolean;
  variablesCollapsed: boolean;
  stepsCollapsed: boolean;
  licenseCollapsed: boolean;
  toggleSection(key: string): () => void;
  showToolbar?: boolean;
  sequencesState: SequenceReducerState;
}

export const SequencePreviewContent = (props: SequencePreviewContentProps) => {
  const { sequence, viewSequenceCeleryScript } = props;
  return <EmptyStateWrapper
    notEmpty={sequence && isTaggedSequence(sequence)}
    graphic={EmptyStateGraphic.sequences}
    title={props.error ? t("Sequence load error") : t("Loading...")}>
    {sequence &&
      <div className={"sequence-preview-content"}>
        {props.showToolbar && props.viewCeleryScript &&
          <PreviewToolbar
            viewSequenceCeleryScript={viewSequenceCeleryScript}
            toggleViewCeleryScript={
              props.toggleSection("viewSequenceCeleryScript")} />}
        <div className={"sequence-editor-sections"}>
          <Description
            collapsed={props.descriptionCollapsed}
            sequence={sequence}
            toggle={props.toggleSection("descriptionCollapsed")} />
          {!viewSequenceCeleryScript &&
            <Variables
              collapsed={props.variablesCollapsed}
              sequence={sequence}
              resources={props.resources}
              toggle={props.toggleSection("variablesCollapsed")} />}
          {viewSequenceCeleryScript &&
            <pre>{stringifySequenceData(sequence.body)}</pre>}
          {!viewSequenceCeleryScript &&
            <Steps
              collapsed={props.stepsCollapsed}
              sequence={sequence}
              resources={props.resources}
              dispatch={props.dispatch}
              sequencesState={props.sequencesState}
              toggle={props.toggleSection("stepsCollapsed")} />}
          <License
            collapsed={props.licenseCollapsed}
            sequence={sequence}
            dispatch={noop}
            toggle={props.toggleSection("licenseCollapsed")} />
        </div>
      </div>}
  </EmptyStateWrapper>;
};

interface ImportBannerProps {
  sequence: TaggedSequence | undefined;
}

export const ImportBanner = (props: ImportBannerProps) => {
  const [importing, setImporting] = React.useState(false);
  const { sequence } = props;
  const includesLua = sequence?.body.body?.map(x => x.kind).includes("lua");
  const navigate = useNavigate();
  return <div className={"import-banner"}>
    <label>{t("viewing a publicly shared sequence")}</label>
    <Help text={Content.IMPORT_SEQUENCE} />
    {sequence &&
      <button className={"transparent-button"}
        onClick={() => {
          installSequence(sequence.body.id)()
            .then(() => navigate(Path.designerSequences()));
          publishAction(setImporting);
        }}>
        {importing ? t("importing") : t("import")}
        {importing && <i className={"fa fa-spinner fa-pulse"} />}
      </button>}
    {includesLua && <p>{t(Content.INCLUDES_LUA_WARNING)}</p>}
  </div>;
};

interface PreviewToolbarProps {
  viewSequenceCeleryScript: boolean;
  toggleViewCeleryScript(): void;
}

const PreviewToolbar = (props: PreviewToolbarProps) =>
  <div className={"preview-toolbar"}>
    <div className={"sequence-editor-tools preview"}>
      <div className={"button-group"}
        style={{ marginBottom: "0", marginTop: "0" }}>
        <i
          className={[
            "fa fa-code fb-icon-button",
            props.viewSequenceCeleryScript ? "" : "inactive",
          ].join(" ")}
          title={t("toggle celery script view")}
          onClick={props.toggleViewCeleryScript} />
      </div>
    </div>
    <hr />
  </div>;

interface SectionBaseProps {
  collapsed: boolean;
  toggle(): void;
  sequence: TaggedSequence;
}

const Description = (props: SectionBaseProps) =>
  <div className={"preview-description"}>
    <SectionHeader title={t("Description")}
      collapsed={props.collapsed}
      toggle={props.toggle} />
    <Collapse isOpen={!props.collapsed}>
      <div className={"sequence-description"}>
        <Markdown>{props.sequence.body.description || ""}</Markdown>
      </div>
    </Collapse>
  </div>;

interface VariablesProps extends SectionBaseProps {
  resources: ResourceIndex;
}

const Variables = (props: VariablesProps) => {
  const variableData = createSequenceMeta(props.resources, props.sequence);
  return <div className={"preview-variables"}>
    <SectionHeader title={t("Variables")}
      collapsed={props.collapsed}
      count={Object.values(variableData).length}
      toggle={props.toggle} />
    <Collapse isOpen={!props.collapsed}>
      <ErrorBoundary>
        <LocalsList
          variableData={variableData}
          sequenceUuid={props.sequence.uuid}
          resources={props.resources}
          onChange={noop}
          allowedVariableNodes={AllowedVariableNodes.parameter} />
      </ErrorBoundary>
    </Collapse>
  </div>;
};

interface StepsProps extends SectionBaseProps {
  resources: ResourceIndex;
  dispatch: Function;
  sequencesState: SequenceReducerState;
}

const Steps = (props: StepsProps) =>
  <div className={"preview-steps"}>
    <SectionHeader title={t("sequence steps")}
      collapsed={props.collapsed}
      count={(props.sequence.body.body || []).length}
      toggle={props.toggle} />
    <Collapse isOpen={!props.collapsed}>
      <div className="sequence" id="sequenceDiv">
        <div className={"sequence-step-components"}>
          <ErrorBoundary>
            <AllSteps
              sequence={props.sequence}
              onDrop={noop}
              dispatch={props.dispatch}
              readOnly={true}
              sequencesState={props.sequencesState}
              resources={props.resources} />
          </ErrorBoundary>
        </div>
      </div>
    </Collapse>
  </div>;

export interface LicenseProps extends SectionBaseProps {
  dispatch: Function;
}

export const License = (props: LicenseProps) => {
  const { sequence, dispatch } = props;
  return <div className={"license"}>
    <SectionHeader title={t("License")}
      collapsed={props.collapsed}
      toggle={props.toggle} />
    <Collapse isOpen={!props.collapsed}>
      <p>{"MIT License"}</p>
      <p>
        {"Copyright (c)"} {moment(sequence.body.created_at).year()}{" "}
        {isSequencePublished(sequence)
          ? <input defaultValue={sequence.body.copyright}
            onChange={e =>
              dispatch(edit(sequence, { copyright: e.currentTarget.value }))} />
          : sequence.body.copyright}
      </p>
      <p>{Content.MIT_LICENSE_PART_1}</p>
      <p>{Content.MIT_LICENSE_PART_2}</p>
      <p>{Content.MIT_LICENSE_PART_3}</p>
    </Collapse>
  </div>;
};