learningequality / kolibri-ecosystem

A repository for tracking issues and new features for the Kolibri Ecosystem
MIT License
0 stars 0 forks source link

🗺 Allow custom navigation of channels/topics in Kolibri #1

Closed rtibbles closed 3 years ago

rtibbles commented 3 years ago

Objective: Create custom navigation experiences in Kolibri to expand the appeal and possibilities for content in Kolibri

Channel creators are able to create custom browsing experiences using HTML5 apps to provide flexibility in display and interaction with the contents of topics (including the channel root topic).

Outcomes

Gherkin Specification

Feature: Custom channels representation

    Scenario: Exploring a custom channel
        Given Custom channels is enabled as an options flag in the Learn plugin
        When The learner clicks on the card for the custom channel
        Then The URL goes to #/topics/<topic_id> and displays a full page HTML5 app

    Scenario: Exploring a disabled custom channel
        Given Custom channels is disbled as an options flag in the Learn plugin
        When The learner clicks on the card for the custom channel
        Then The URL goes to #/topics/<topic_id> and displays the Kolibri topic interface

    Scenario: Navigating in a custom channel
        Given The learner has started exploring a custom channel
        When The learner clicks on a topic in the custom navigation
        And The HTML5 app displays the contents of the topic
        Then The URL updates with new context and stays on the full page HTML5 app

    Scenario: Showing resources in a custom channel
        Given The learner has started exploring a custom channel
        When The learner clicks on a resource in the custom navigation
        Then An overlay showing the content displays over the full page HTML5 app
        And The URL updates with new context
        And The full page HTML5 app remains in the background

    Scenario: User closing shown resources in a custom channel
        Given The learner has opened a resource from within a custom channel
        When The learner clicks close on the overlay
        Then The URL updates with new context
        And The overlay closes
        And The full page HTML5 app is still displayed

    Scenario: Custom nav closing shown resources in a custom channel
        Given The learner has opened a resource from within a custom channel
        When The custom nav app says the overlay should close
        Then The URL updates with new context
        And The overlay closes
        And The full page HTML5 app is still displayed

    Scenario: Navigating out of a custom channel
        Given The learner has started exploring a custom channel
        When The learner clicks on a link in the custom navigation
        And The link is to a topic
        Then The URL goes to #/topics/t/<topic_id> and displays the Kolibri topic interface
        And The full page HTML5 app closes

    Scenario: Navigating between resources
        Given The learner has started exploring a resource with links
        When The learner clicks on a link in the resource
        Then The URL goes to #/topics/c/<node_id> and displays the Kolibri content page
        And The old content page closes

API Spec for Hashi 'kolibri' namespace object

/**
 * This class offers an API interface for interacting directly with the Kolibri app
 * that the HTML5 app is embedded within
 */
import BaseShim from './baseShim';

/**
 * Type definition for Language metadata
 * @typedef {Object} Language
 * @property {string} id - an IETF language tag
 * @property {string} lang_code - the ISO 639‑1 language code
 * @property {string} lang_subcode - the regional identifier
 * @property {string} lang_name - the name of the language in that language
 * @property {('ltr'|'rtl'|)} lang_direction - Direction of the language's script,
 * top to bottom is not supported currently
 */

/**
 * Type definition for ContentNode metadata
 * @typedef {Object} ContentNode
 * @property {string} id - unique id of the ContentNode
 * @property {string} channel_id - unique channel_id of the channel that the ContentNode is in
 * @property {string} content_id - identifier that is common across all instances of this resource
 * @property {string} title - A title that summarizes this ContentNode for the user
 * @property {string} description - detailed description of the ContentNode
 * @property {string} author - author of the ContentNode
 * @property {string} thumbnail_url - URL for the thumbnail for this ContentNode,
 * this may be any valid URL format including base64 encoded or blob URL
 * @property {boolean} available - Whether the ContentNode has all necessary files for rendering
 * @property {boolean} coach_content - Whether the ContentNode is intended only for coach users
 * @property {Language} lang - The primary language of the ContentNode
 * @property {string} license_description - The description of the license, which may be localized
 * @property {string} license_name - The human readable name of the license, localized
 * @property {string} license_owner - The name of the person or organization that holds copyright
 * @property {number} num_coach_contents - Number of coach contents that are descendants of this
 * @property {string} parent - The unique id of the parent of this ContentNode
 * @property {number} sort_order - The order of display for this node in its channel
 * if depth recursion was not deep enough
 */

/**
 * Type definition for pagination object
 * @typedef {Object} PageResult
 * @property {number} page - the page that this pagination object represents
 * @property {number} pageSize - the page size for this pagination object
 * @property {ContentNode[]} results - the array of ContentNodes for this page
 */

/**
 * Type definition for Theme options
 * properties TBD
 * @typedef {Object} Theme
 */

function encodeContext(context) {
  return encodeURI(Object.entries(context).map(([k,v]) => `${k}:${v}`));
}

 /**
 * Type definition for NavigationContext
 * This can have arbitrary properties as defined
 * by the navigating app that it uses to resume its state
 * Should be able to be encoded down to <1600 characters using
 * an encoding function something like 'encode context' above
 * @typedef {Object} NavigationContext
 * @property {string} node_id - The current node_id that is being displayed, custom apps should handle
 * this as it may be used to generate links externally to jump to this state
 */

export default class Kolibri extends BaseShim {
  constructor(mediator) {
    super(mediator);
    this.nameSpace = 'kolibri';
  }

  iframeInitialize(contentWindow) {
    this.__setShimInterface();
    Object.defineProperty(contentWindow, this.nameSpace, {
      value: this.shim,
      configurable: true,
    });
  }

  __setShimInterface() {
    const self = this;

    class Shim {
      /*
       * Method to query contentnodes from Kolibri and return
       * an array of matching metadata
       * @param {Object} options - The different options to filter by
       * @param {string=} options.parent - id of the parent node to filter by, or 'self'
       * @param {string[]} options.ids - an array of ids to filter by
       * @param {string[]} options.tags - an array of tags to do an OR filter by,
       * returning nodes that match any of the tags
       * @param {number} [options.page=1] - which page to return from the result set
       * @param {number} [options.pageSize=50] - the page size for pagination
       * @return {Promise<PageResult>} - a Promise that resolves to an array of ContentNodes
       */
      getContentByFilter(options) {}

      /*
       * Method to query a single contentnodes from Kolibri and return
       * a metadata object
       * @param {string} id - id of the ContentNode
       * @return {Promise<ContentNode>} - a Promise that resolves to a ContentNode
       */
      getContentById(id) {}

      /*
       * Method to search for contentnodes on Kolibri and return
       * an array of matching metadata
       * @param {Object} options - The different options to search by
       * @param {string=} options.keyword - search term for key word search
       * @param {string=} options.under - id of topic to search under, or 'self'
             * @param {number} [options.page=1] - which page to return from the result set
             * @param {number} [options.pageSize=50] - the page size for pagination
       * @return {Promise<PageResult>} - a Promise that resolves to an array of ContentNodes
       */
      searchContent(options) {}

      /*
       * Method to set a default theme for any content rendering initiated by this app
       * @param {Theme} options - The different options for custom themeing
       * @return {Promise} - a Promise that resolves when the theme has been applied
       */
      themeRenderer(options) {}

      /*
       * Method to allow navigation to or rendering of a specific node
       * has optional parameter context that can update the URL for a custom context.
       * When this is called for a resource node in the custom navigation context
       * this will launch a renderer overlay to maintain the current state, and update the
       * query parameters for the URL of the custom context to indicate the change
       * If called for a topic in a custom context or outside of a custom context
       * this will simply prompt navigation to that node in Kolibri.
       * @param {string} nodeId - id of the parent node to navigate to
       * @param {NavigationContext=} context - optional context describing the state update
             * if node_id is missing from the context, it will be automatically filled in by this method
       * @return {Promise} - a Promise that resolves when the navigation has completed
       */
      navigateTo(nodeId, context = {}) {}

      /*
       * Method to allow updating of stored state in the URL
       * @param {NavigationContext} context - context describing the state update
       * @return {Promise} - a Promise that resolves when the context has been updated
       */
      updateContext(context) {}

      /*
       * Method to request the current context state
       * @return {Promise<NavigationContext>} - a Promise that resolves when the context has been updated
       */
      getContext() {}

      /*
       * Getter to return the current version of Kolibri and hence the API available.
       * @return {string} - A version string
       */
            get version() {}

    }
    this.shim = new Shim();

    return this.shim;
  }
}
rtibbles commented 3 years ago

This has now been implemented.