concord-consortium/lara

View on GitHub
lara-typescript/src/section-authoring/components/item-edit-dialog.tsx

Summary

Maintainability
D
1 day
Test Coverage
import * as React from "react";
import { useEffect, useCallback, useState } from "react";
import { ISectionItem, ITextBlockData } from "../api/api-types";
import { Modal, ModalButtons } from "../../shared/components/modal/modal";
import { TextBlockEditForm } from "./text-block-edit-form";
import { IManagedInteractive, ManagedInteractiveAuthoring } from "../../page-item-authoring/managed-interactives";
import { IMWInteractive, MWInteractiveAuthoring } from "../../page-item-authoring/mw-interactives";
import { PluginAuthoring } from "./plugin-authoring";
import { Save } from "../../shared/components/icons/save-icon";
import { Close } from "../../shared/components/icons/close-icon";
import { usePageAPI } from "../hooks/use-api-provider";
import { UserInterfaceContext } from "../containers/user-interface-provider";
import { camelToSnakeCaseKeys } from "../../shared/convert-keys";
import { TextBlockPreview } from "./text-block-preview";
import { ManagedInteractivePreview } from "./managed-interactive-preview";
import { MWInteractivePreview } from "./mw-interactive-preview";
import classNames from "classnames";

import "./item-edit-dialog.scss";

export interface IItemEditDialogProps {
  errorMessage?: string;
}

export const ItemEditDialog: React.FC<IItemEditDialogProps> = ({
  errorMessage
  }: IItemEditDialogProps) => {
  const {
    userInterface: {editingItemId, wrappedItemId},
    actions: {setEditingItemId, setWrappedItemId}
  } = React.useContext(UserInterfaceContext);
  const { getItems, updatePageItem, getLibraryInteractives } = usePageAPI();
  const pageItems = getItems();
  const pageItem = pageItems.find(pi => pi.id === editingItemId);
  const wrappedItem = pageItems.find(pi => pi.id === wrappedItemId);
  const [previewPageItem, setPreviewPageItem] = useState<ISectionItem>();
  const [itemData, setItemData] = useState({});
  const libraryInteractives = getLibraryInteractives.data?.libraryInteractives;

  useEffect(() => {
    if (Object.keys(itemData).length > 0) {
      handleUpdateItem();
    }
  }, [itemData]);

  useEffect(() => {
    setItemData({});
  }, [editingItemId]);

  useEffect(() => {
    if (!previewPageItem) {
      setPreviewPageItem(pageItem);
    }
  }, [pageItem]);

  const handleUpdateTextBlockData = (updates: ITextBlockData) => {
    setItemData(updates);
  };

  const handleManagedInteractiveData = (updates: Partial<IManagedInteractive>) => {
    const newData = {...itemData, ...updates};
    setItemData(newData);
  };

  const handleMwInteractiveData = (updates: Partial<IMWInteractive>) => {
    const newData = {...itemData, ...updates};
    setItemData(newData);
  };

  const handlePluginData = (authorData: string) => {
    setItemData({authorData});
  };

  const handleUpdateItem = () => {
    if (pageItem) {
      const pageItemUpdateOpts = {...pageItem};
      pageItemUpdateOpts.data = {...pageItem.data, ...itemData};
      updatePageItem(pageItemUpdateOpts);
    }
    handleCloseDialog();
  };

  const handleUpdateItemPreview = useCallback((updates: Record<string, any> | Partial<IManagedInteractive>) => {
    if (previewPageItem) {
      const data = {...previewPageItem.data, ...updates};
      setPreviewPageItem({...previewPageItem, data});
    } else if (pageItem) {
      const data = {...pageItem.data, ...updates};
      setPreviewPageItem({...pageItem, data});
    }
  }, [pageItem, previewPageItem]);

  const handleBooleanElement = (element: HTMLInputElement) => {
    // boolean value form fields are of type radio or of type hidden
    const elementValue = element.type === "radio" ?
      (document.querySelector(`input[name="${element.name}"]:checked`) as HTMLInputElement).value
      : element.value;
    return elementValue === "true" || elementValue === "1";
  };

  const handleSave = () => {
    const form = document.getElementById("itemEditForm");
    form?.dispatchEvent(new Event("submit"));
  };

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formElements = e.currentTarget.elements;
    const updates: any = {};
    Array.prototype.forEach.call(formElements, (element: HTMLInputElement | HTMLTextAreaElement) => {
      if (element.name && element.name !== "") {
        // some values need to be converted from strings to booleans or numbers
        let elementValue: string | number | boolean = "";
        switch (element.name) {
          case "inherit_aspect_ratio_method":
          case "inherit_click_to_play":
          case "inherit_click_to_play_prompt":
          case "inherit_full_window":
          case "inherit_image_url":
          case "inherit_native_height":
          case "inherit_native_width":
            elementValue = handleBooleanElement((element as HTMLInputElement));
            break;
          case "library_interactive_id":
            elementValue = parseInt(element.value, 10);
            break;
          default:
            elementValue = element.type === "checkbox" ? (element as HTMLInputElement).checked : element.value;
        }
        updates[element.name] = elementValue;
      }
    });
    switch (pageItem?.type) {
      case "Embeddable::Xhtml":
        handleUpdateTextBlockData(updates);
        break;
      case "ManagedInteractive":
        handleManagedInteractiveData(updates);
        break;
      case "MwInteractive":
        handleMwInteractiveData(updates);
        break;
    }
  };

  const handleCancelUpdateItem = () => {
    handleCloseDialog();
  };

  const handleCloseDialog = () => {
    setEditingItemId(false);
    setWrappedItemId(false);
    setItemData({});
    setPreviewPageItem(undefined);
  };

  const interactiveFromItemToEdit = (itemToEdit: ISectionItem) => {
    const interactive = camelToSnakeCaseKeys(itemToEdit.data);
    interactive.interactive_item_id = `interactive_${itemToEdit.id}`;
    return interactive;
  };

  const standardModalButtons = [
    {
      classes: "cancel",
      clickHandler: handleCancelUpdateItem,
      disabled: false,
      svg: <Close height="12" width="12"/>,
      text: "Cancel"
    },
    {
      classes: "save",
      clickHandler: handleSave,
      disabled: false,
      svg: <Save height="16" width="16"/>,
      text: "Save"
    }
  ];

  let modalButtons = standardModalButtons;
  if (pageItem && pageItem.type === "Embeddable::EmbeddablePlugin") {
    // The authoring form of plugins supply their own buttons,
    // So we remove the standard buttons from the modal.
    const pluginModalButtons: any[] = [];
    modalButtons = pluginModalButtons;
  }
  const getEditForm = (itemToEdit: ISectionItem) => {
    const authoringApiUrls = itemToEdit.authoring_api_urls ? itemToEdit.authoring_api_urls : {};
    switch (itemToEdit.type) {
      case "Embeddable::Xhtml":
        return <TextBlockEditForm
                 pageItem={itemToEdit}
                 handleUpdateItemPreview={handleUpdateItemPreview}
               />;
      case "ManagedInteractive":
        const managedInteractive = interactiveFromItemToEdit(itemToEdit);
        const libraryInteractive = camelToSnakeCaseKeys(
                                     libraryInteractives?.find(
                                       li => li.id === itemToEdit.data.libraryInteractiveId
                                     )
                                   );
        return <ManagedInteractiveAuthoring
                managedInteractive={managedInteractive}
                libraryInteractive={libraryInteractive}
                defaultClickToPlayPrompt={"Click to Play"}
                authoringApiUrls={authoringApiUrls}
                onUpdate={handleManagedInteractiveData}
                handleUpdateItemPreview={handleUpdateItemPreview}
               />;
      case "MwInteractive":
        const interactive = interactiveFromItemToEdit(itemToEdit);
        return <MWInteractiveAuthoring
                interactive={interactive}
                defaultClickToPlayPrompt={"Click to Play"}
                authoringApiUrls={authoringApiUrls}
                handleUpdateItemPreview={handleUpdateItemPreview}
               />;
      case "Embeddable::EmbeddablePlugin":
        return <PluginAuthoring
          pageItem={itemToEdit}
          authoringApiUrls={authoringApiUrls}
          onUpdate={handlePluginData}
          closeForm={handleCancelUpdateItem}
          wrappedItem={wrappedItem}
          />;
      default:
        return "Editing not supported.";
    }
  };

  const supportsPreview = () => {
    return pageItem && pageItem.type === "Embeddable::Xhtml" ||
           pageItem && pageItem.type === "ManagedInteractive" ||
           pageItem && pageItem.type === "MwInteractive";
  };

  const getPreview = () => {
    const previewNote = <p className="previewNote">
      This preview does not yet reflect all features and settings available in the edit form.
      To view exactly how this assessment item will appear in runtime, please save your changes
      and preview the activity in Activity Player.
    </p>;
    if (pageItem && previewPageItem) {
      switch (pageItem.type) {
        case "Embeddable::Xhtml":
          return <TextBlockPreview pageItem={previewPageItem} />;
        case "ManagedInteractive":
          return <>
            <ManagedInteractivePreview
              pageItem={previewPageItem}
            />
            {previewNote}
          </>;
        case "MwInteractive":
          return <>
            <MWInteractivePreview
              pageItem={previewPageItem}
            />
            {previewNote}
          </>;
        default:
          return `Preview not supported for item type ${pageItem.type}.`;
      }
    }
  };

  if (pageItem) {
    const formClassName = classNames({noPreview: !supportsPreview()});
    return (
      <Modal
        title="Edit"
        className="itemEditDialog"
        closeFunction={handleCancelUpdateItem}
        visibility={true}
      >
        <div id="itemEditDialog">
          {errorMessage &&
            <div className="errorMessage">
              {errorMessage}
            </div>
          }
          <form id="itemEditForm" onSubmit={handleSubmit} className={formClassName}>
            {getEditForm(pageItem)}
            <ModalButtons buttons={modalButtons} />
          </form>
            {supportsPreview() &&
              <div className="itemEditPreview">
                <h2>Preview</h2>
                  <div className="itemEditPreviewContent">
                  {getPreview()}
                </div>
              </div>
            }
        </div>
      </Modal>
    );
  }
  return null;
};