concord-consortium/lara

View on GitHub
lara-typescript/src/section-authoring/api/mock-api-provider.ts

Summary

Maintainability
F
5 days
Test Coverage
import { findItemAddress, findItemByAddress } from "../util/finding-utils";
import { setSectionPositions, updatePositions } from "../util/move-utils";
import {
  IPage, PageId,
  APIPageGetF, APIPagesGetF, APIPageItemUpdateF,
  IAuthoringAPIProvider, ISection, ICreatePageItem, ISectionItem, SectionColumns,
  ISectionItemType, APIPageItemDeleteF, ItemId, SectionId, SectionLayouts, ILibraryInteractive,
  IPortal, ISectionItemPlugin, IPlugin, IPluginEmbeddable, IEmbeddableMetaData
} from "./api-types";
import { IManagedInteractive } from "../../page-item-authoring/managed-interactives";

let pageCounter = 0;
let sectionCounter = 1;
let itemCounter = 1;
let managedInteractiveCounter = 0;

let pages: IPage[] = [
  {
    id: `${pageCounter++}`,
    name: `Page ${pageCounter}`,
    sections: []
  }
];

const getPreviewOptions = (args: {pageId: PageId}): Promise<Record<string, string>> => {
  return Promise.resolve({
    "Select an option...": "",
    "Fake Activity Player": "https://activity-player.concord.org/branch/master",
    "Fake Activity Player Teachers": "https://activity-player.concord.org/branch/master"
  });
};

const makeNewSection = (): ISection => {
  const section: ISection = {
    id: `${++sectionCounter}`,
    items: [],
    layout: SectionLayouts.LAYOUT_FULL_WIDTH
  };
  return section;
};

const makeNewPageItem = (attributes: ICreatePageItem): ISectionItem => {
  const returnAttributes = {...attributes, type: makePageItemType(attributes.type)};
  const newItem: ISectionItem = {
    id: `${++itemCounter}`,
    embeddableId: `123${itemCounter}`,
    column: attributes.column || SectionColumns.PRIMARY,
    data: makeNewEmbeddable(returnAttributes),
    position: attributes.position || 1,
    type: returnAttributes.type
  };
  return newItem;
};

const makePageItemType = (pickerType: string | undefined) => {
  switch (pickerType) {
    case "LibraryInteractive":
      return "ManagedInteractive";
    default:
      return pickerType;
  }
};

const makeNewEmbeddable = (attributes: ICreatePageItem) => {
  const itemType = attributes.type;

  switch (itemType) {
    case "Embeddable::Xhtml":
      return ({});
      break;
    case "ManagedInteractive":
      const [libraryInteractiveType, libraryInteractiveId] = attributes.embeddable.split("_");
      return (
        {
          id: managedInteractiveCounter++,
          library_interactive_id: parseInt(libraryInteractiveId, 10),
          name: "",
          url_fragment: "",
          authored_state: "",
          is_hidden: false,
          is_half_width: false,
          aspect_ratio: 1,
          enable_learner_state: false,
          linked_interactive_type: "",
          show_in_featured_question_report: true,
          inherit_aspect_ratio_method: true,
          custom_aspect_ratio_method: "",
          inherit_native_width: true,
          custom_native_width: 576,
          inherit_native_height: true,
          custom_native_height: 435,
          inherit_click_to_play: true,
          custom_click_to_play: false,
          inherit_full_window: true,
          custom_full_window: false,
          inherit_click_to_play_prompt: true,
          custom_click_to_play_prompt: "",
          inherit_image_url: true,
          custom_image_url: "",
          interactive_item_id: "",
          linked_interactive_item_id: "",
          linked_interactives: []
        }) as Partial<IManagedInteractive>;
      break;
    default:
      return ({});
  }
};

export const updatePageItem: APIPageItemUpdateF = (args: {pageId: string, sectionItem: ISectionItem}) => {
  const { pageId, sectionItem } = args;
  const page = pages.find(p => p.id === pageId);
  let item: ISectionItem | undefined;
  page?.sections.forEach((s) => {
    item = s.items?.find(i => i.id === sectionItem.id);
  });
  if (item) {
    const updatedItem: ISectionItem = {
      id: sectionItem.id,
      embeddableId: sectionItem.embeddableId,
      column: sectionItem.column || SectionColumns.PRIMARY,
      data: sectionItem.data,
      position: sectionItem.position || 1,
      type: sectionItem.type
    };
    item = updatedItem;
    if (page) {
      updatePage({pageId: page.id, changes: page});
    }
    return Promise.resolve(updatedItem);
  }
  return Promise.reject("something went wrong");
};

export const getPages: APIPagesGetF = () => {
  return Promise.resolve(pages);
};

export const getPage: APIPageGetF = (id: PageId) => {
  return Promise.resolve(pages.find(p => p.id === id) || null);
};

export const createPage = () => {
  const newPage: IPage = {
    id: `${++pageCounter}`,
    name: `Page ${pageCounter}`,
    sections: []
  };
  // we can assume the completion page is always the last element of the array before new page was added
  if (pages[pages.length - 1].isCompletion) {
    pages.splice(pages.length - 1, 0, newPage);
  } else {
    pages.push(newPage);
  }
  updatePositions(pages);
  return Promise.resolve(newPage);
};

export const deletePage = (id: PageId) => {
  pages = pages.filter(p => p.id !== id);
  return Promise.resolve(pages);
};

const copyPage = (args: {pageId: PageId, destIndex: number}) => {
  const {pageId, destIndex} = args;
  let newDestIndex;
  const page = pages.find(p => p.id === pageId);
  if (page) {
    const nextPage = {...page};
    nextPage.id = `${++pageCounter}`;
    nextPage.sections = page.sections.map( s => {
      const items = s.items?.map(item => {
        const id = `${++itemCounter}`;
        return {...item, id };
      });
      return { ...s, items };
    });
    if (destIndex === -1 || destIndex === 0) {
      newDestIndex = destIndex + 1;
    } else {
      newDestIndex = destIndex;
    }
    pages.splice(newDestIndex, 0, nextPage);
    updatePositions(pages);
    return Promise.resolve(nextPage);
  }
  return Promise.reject("no source page in copy");
};

export const updatePage = (args: {pageId: PageId, changes: Partial<IPage>}) => {
  const {pageId, changes} = args;
  const indx = pages.findIndex(p => p.id === pageId);
  if (indx > -1) {
    if (indx < pages.length - 1 && changes.isCompletion) {
      pages.push(pages.splice(indx, 1)[0]); // put the page at the end of the pages list
    }
    const newIndx = pages.findIndex(p => p.id === pageId); // we do this again because pages may have reordered
    const nextPage = {...pages[newIndx], ...changes };
    pages[newIndx] = nextPage;
    setSectionPositions(nextPage);
    return Promise.resolve({... nextPage});
  }
  return Promise.reject("Can't find that page");
};

const createSection = (id: PageId) => {
  const page = pages.find(p => p.id === id);
  if (page) {
    page.sections.push(makeNewSection());
    setSectionPositions(page);
    return Promise.resolve(page);
  }
  return Promise.reject(`cant find page ${id}`);
};

const updateSections = (nextPage: IPage) => {
  const existingPage = pages.find(p => p.id === nextPage.id);
  if (existingPage) {
    updatePage({pageId: existingPage.id, changes: existingPage});
  }
  return Promise.resolve(nextPage);
};

const updateSection = (args: {pageId: PageId, changes: { section: Partial<ISection> }}) => {
  const {pageId, changes} = args;
  const page = pages.find(p => p.id === pageId);
  if (page) {
    const section  = page.sections.find(s => s.id === changes.section.id);
    if (section) {
      Object.assign(section, changes.section);
    }
    return Promise.resolve(page);
  }
  return Promise.reject(`cant find page ${pageId}`);
};

const copySection = (args: {pageId: PageId, sectionId: SectionId}) => {
  const {pageId, sectionId} = args;
  const page = pages.find(p => p.id === pageId);
  if (!page) {
    return Promise.reject(`can't find page: ${pageId}`);
  }
  const address = findItemAddress({pages, sectionId});

  // updates position, assumes the array is in the right order.
  const reorderSection = (sections: ISection[]) => {
    sections.forEach((s, i) => {
      s.position = i;
    });
  };

  // Deep Clone object:
  const clone = <T>(source: T): T => {
    return JSON.parse(JSON.stringify(source));
  };

  if (! (address.pageIndex === null || address.sectionIndex ===  null)) {
    const section = page.sections[address.sectionIndex];
    const newSection = clone(section);
    newSection.id = `${sectionCounter++}`;
    newSection.position = (newSection.position || 0) + 1;
    newSection.items?.forEach(i => {
      i.id = `${itemCounter++}`;
    });
    const start = page.sections.findIndex(i => i.id === sectionId);
    if (start > -1) {
      page.sections.splice(start + 1, 0, newSection);
      reorderSection(page.sections);
    }
  }
  return Promise.resolve(page);
};

const createPageItem = (args: {pageId: PageId, newPageItem: ICreatePageItem}) => {
  const {newPageItem, pageId} = args;
  const sectionId = newPageItem.section_id;
  const page = pages.find(p => p.id === pageId);
  if (page) {
    const section = page.sections.find(s => s.id === sectionId);
    if (section) {
      const newlyCreatedPageItem = makeNewPageItem(newPageItem);
      section.items?.push(newlyCreatedPageItem);
      return Promise.resolve(newlyCreatedPageItem);
    }
    return Promise.reject(`cant find section ${sectionId}`);
  }
  return Promise.reject(`cant find page ${pageId}`);
};

const deletePageItem: APIPageItemDeleteF = (args: {pageId: PageId, pageItemId: ItemId}) => {
  const { pageId, pageItemId } = args;
  const page = pages.find(p => p.id === pageId);
  if (page) {
    let replacementSection: ISection | null = null;
    page?.sections.forEach(s => {
      s.items?.forEach(i => {
        if (i.id === pageItemId) {
          replacementSection = s;
        }
      });
    });

    if (replacementSection) {
      (replacementSection as ISection).items = (replacementSection as ISection).items?.filter(i => i.id !== pageItemId);
    }
    return Promise.resolve(page);
  }
  return Promise.reject(`cant find page ${pageId}`);
};

const getAllEmbeddables = () => {
  const allEmbeddables: Array<ISectionItemType|ISectionItemPlugin> = [
    {
      id: "1",
      serializeable_id: "LibraryInteractive_1",
      name: "Carousel",
      type: "LibraryInteractive",
      useCount: 1,
      dateAdded: 1630440496,
    },
    {
      id: "2",
      serializeable_id: "LibraryInteractive_2",
      name: "CODAP",
      type: "LibraryInteractive",
      useCount: 5,
      dateAdded: 1630440497
    },
    {
      id: "3",
      serializeable_id: "LibraryInteractive_3",
      name: "Drag & Drop",
      type: "LibraryInteractive",
      useCount: 5,
      dateAdded: 1630440498
    },
    {
      id: "4",
      serializeable_id: "LibraryInteractive_4",
      name: "Fill in the Blank",
      type: "LibraryInteractive",
      useCount: 8,
      dateAdded: 1630440495
    },
    {
      id: "5",
      serializeable_id: "MwInteractive_5",
      name: "iFrame Interactive",
      type: "MwInteractive",
      useCount: 200,
      dateAdded: 1630440494,
      isQuickAddItem: true
    },
    {
      id: "6",
      serializeable_id: "LibraryInteractive_6",
      name: "Multiple Choice",
      type: "LibraryInteractive",
      useCount: 300,
      dateAdded: 1630440493
    },
    {
      id: "7",
      serializeable_id: "LibraryInteractive_7",
      name: "Open Response",
      type: "LibraryInteractive",
      useCount: 400,
      dateAdded: 1630440492,
      isQuickAddItem: true
    },
    {
      id: "8",
      serializeable_id: "LibraryInteractive_8",
      name: "SageModeler",
      type: "LibraryInteractive",
      useCount: 3,
      dateAdded: 1630440499
    },
    {
      id: "9",
      serializeable_id: "LibraryInteractive_9",
      name: "Teacher Edition Window Shade",
      type: "LibraryInteractive",
      useCount: 4,
      dateAdded: 1630440490
    },
    {
      id: "10",
      serializeable_id: "Embeddable::Xhtml_10",
      name: "Text Block",
      type: "Embeddable::Xhtml",
      useCount: 500,
      dateAdded: 1630440491,
      isQuickAddItem: true
    },
    {
      id: "11",
      serializeable_id: "Plugin::TeacherEditionWindowShade_11",
      name: "Window Shade",
      type: "plugin",
      useCount: 5,
      dateAdded: 1630440491,
      isQuickAddItem: false,
      components: [{
        label: "Teacher Tips",
        name: "Activity",
        scope: "embeddable",
        guiAuthoring: true,
        guiPreview: true
      }]
    }
  ];
  return Promise.resolve({allEmbeddables});
};

const getLibraryInteractives = () => {
  const libraryInteractives: ILibraryInteractive[] = [
    {
      id: 1,
      serializeable_id: "LibraryInteractive_1",
      name: "Carousel",
      type: "LibraryInteractive",
      use_count: 1,
      date_added: 1630440496,
      aspect_ratio_method: "DEFAULT",
      authorable: true,
      authoring_guidance: "",
      base_url: "https://localhost:8081/carousel/",
      click_to_play: false,
      click_to_play_prompt: "",
      created_at: "2020-07-27T16:33:01Z",
      customizable: true,
      description: "",
      enable_learner_state: true,
      export_hash: "ccdfa2d588c34914cb072ef5e88834bce7e0702a",
      full_window: false,
      has_report_url: false,
      image_url: "",
      native_height: 435,
      native_width: 576,
      no_snapshots: false,
      official: false,
      show_delete_data_button: false,
      thumbnail_url: "",
      updated_at: "2021-05-04T21:26:18Z"
    },
    {
      id: 3,
      serializeable_id: "LibraryInteractive_3",
      name: "Drag & Drop",
      type: "LibraryInteractive",
      use_count: 5,
      date_added: 1630440498,
      aspect_ratio_method: "DEFAULT",
      authorable: true,
      authoring_guidance: "",
      base_url: "https://localhost:8081/drag-and-drop/",
      click_to_play: false,
      click_to_play_prompt: "",
      created_at: "2020-07-27T16:33:01Z",
      customizable: true,
      description: "",
      enable_learner_state: true,
      export_hash: "ccdfa2d588c34914cb072ef5e88834bce7e0702b",
      full_window: false,
      has_report_url: false,
      image_url: "",
      native_height: 435,
      native_width: 576,
      no_snapshots: false,
      official: false,
      show_delete_data_button: false,
      thumbnail_url: "",
      updated_at: "2021-05-04T21:26:18Z"
    },
    {
      id: 4,
      serializeable_id: "LibraryInteractive_4",
      name: "Fill in the Blank",
      type: "LibraryInteractive",
      use_count: 8,
      date_added: 1630440495,
      aspect_ratio_method: "DEFAULT",
      authorable: true,
      authoring_guidance: "",
      base_url: "https://localhost:8081/fill-in-the-blank/",
      click_to_play: false,
      click_to_play_prompt: "",
      created_at: "2020-07-27T16:33:01Z",
      customizable: true,
      description: "",
      enable_learner_state: true,
      export_hash: "ccdfa2d588c34914cb072ef5e88834bce7e0702c",
      full_window: false,
      has_report_url: false,
      image_url: "",
      native_height: 435,
      native_width: 576,
      no_snapshots: false,
      official: false,
      show_delete_data_button: false,
      thumbnail_url: "",
      updated_at: "2021-05-04T21:26:18Z"
    },
    {
      id: 6,
      serializeable_id: "LibraryInteractive_6",
      name: "Multiple Choice",
      type: "LibraryInteractive",
      use_count: 300,
      date_added: 1630440493,
      aspect_ratio_method: "DEFAULT",
      authorable: true,
      authoring_guidance: "",
      base_url: "https://localhost:8081/multiple-choice/",
      click_to_play: false,
      click_to_play_prompt: "",
      created_at: "2020-07-27T16:33:01Z",
      customizable: true,
      description: "",
      enable_learner_state: true,
      export_hash: "ccdfa2d588c34914cb072ef5e88834bce7e0702d",
      full_window: false,
      has_report_url: false,
      image_url: "",
      native_height: 435,
      native_width: 576,
      no_snapshots: false,
      official: false,
      show_delete_data_button: false,
      thumbnail_url: "",
      updated_at: "2021-05-04T21:26:18Z"
    },
    {
      id: 7,
      serializeable_id: "LibraryInteractive_7",
      name: "Open Response",
      type: "LibraryInteractive",
      use_count: 400,
      date_added: 1630440492,
      isQuickAddItem: true,
      aspect_ratio_method: "DEFAULT",
      authorable: true,
      authoring_guidance: "",
      base_url: "https://localhost:8081/open-response/",
      click_to_play: false,
      click_to_play_prompt: "",
      created_at: "2020-07-27T16:33:01Z",
      customizable: true,
      description: "",
      enable_learner_state: true,
      export_hash: "ccdfa2d588c34914cb072ef5e88834bce7e0702e",
      full_window: false,
      has_report_url: false,
      image_url: "",
      native_height: 435,
      native_width: 576,
      no_snapshots: false,
      official: false,
      show_delete_data_button: false,
      thumbnail_url: "",
      updated_at: "2021-05-04T21:26:18Z"
    }
  ];
  return Promise.resolve({libraryInteractives});
};

const getAvailablePlugins = () => {
  const plugins: IPlugin[] = [
    {
      id: "1",
      name: "Fake Plugin"
    }
  ];
  return Promise.resolve({plugins});
};

const getPageItemPlugins = () => {
  const pageItemPlugins: IPluginEmbeddable[] = [
    {
      embeddableId: "123",
      id: "1",
      name: "Fake Plugin",
      sectionItemId: "567"
    }
  ];
  return Promise.resolve({pageItemPlugins});
};

const getPageItemEmbeddableMetaData = () => {
  const pageItemEmbeddableData: IEmbeddableMetaData = {
    embeddableId: "123",
    embeddableType: "Fake Type"
  };
  return Promise.resolve(pageItemEmbeddableData);
};

const getPortals = () => {
  const portals: IPortal[] = [
    {
      name: "Fake Portal",
      path: "#"
    }
  ];
  return Promise.resolve({portals});
};

const copyPageItem = (args: {pageId: PageId, sectionItemId: ItemId}) => {
  const {sectionItemId, pageId} = args;
  const page = pages.find(p => p.id === pageId);
  let nextItem: ISectionItem | null = null;
  let destSection: ISection | null = null;
  if (page) {
    sectionLoop:
    for (const pageSection of page.sections) {
      for (const item of pageSection.items || []) {
        if (item.id === sectionItemId) {
          nextItem = {...item};
          nextItem.id = `${++itemCounter}`;
          destSection = pageSection;
          break sectionLoop;
        }
      }
    }
    if (nextItem && destSection) {
      (destSection.items || []).push(nextItem);
      return Promise.resolve(nextItem);
    }
  }
  return Promise.reject(`cant find page:${pageId}, item: ${sectionItemId}`);
};

export const API: IAuthoringAPIProvider = {
  getPages, getPage, createPage, updatePage, deletePage, copyPage,
  createSection, updateSections, updateSection, copySection,
  createPageItem, updatePageItem, deletePageItem, copyPageItem,
  getAllEmbeddables, getLibraryInteractives, getAvailablePlugins, getPortals,
  getPreviewOptions, getPageItemPlugins, getPageItemEmbeddableMetaData,
  pathToTinyMCE: "https://cdnjs.cloudflare.com/ajax/libs/tinymce/5.10.0/tinymce.min.js", pathToTinyMCECSS: undefined,
  isAdmin: false
};