Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First updates to hashi documentation #8086

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions docs/frontend_architecture/HTML5_API.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
Custom Navigation
=================

The purpose of the ``kolibri.js`` extension of our HTML5 API is to allow a sandboxed HTML5 app to safely request the main Kolibri application's data.

External/partner product teams can create HTML5 applications that are fully embeddable within Kolibri and can read Kolibri content data, which they otherwise wouldn't be able to access. This opens up possibilities for creative ways in which learners can engage with content, because partners can create any type of app they want. The app could be something completely new, developed for a content source that we are adding to the platform, or it could be a branded, offline recreation of a partner's existing learning app that previously would not have been able to exist on Kolibri.

When a user has permissions to access a custom channel, and they click on it in the main learn tab, rather than viewing `normal Kolibri`, they will experience a full-screen HTML5 app. One `out-of-the-box` user interaction is the ``navigateTo()`` function, which opens a modal that displays a content node. For other data fetching requests, the app, not Kolibri, has the responsibilty of determining what to do with that data.


Basic API
~~~~~~~~~


Access the API from within an HTML5 app by using ``window.kolibri.[function]``

Functions:

.. code-block:: javascript

/**
* 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 PageResults array
* @property {ContentNode[]} results - the array of ContentNodes for this page
* This will be updated to a Pagination Object once pagination is implemented
*/

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

/**
* 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
*/

/*
* 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
* @return {Promise<PageResult>} - a Promise that resolves to an array of ContentNodes
*/
getContentByFilter(options)

/*
* Method to query a single contentnode 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'
* @return {Promise<PageResult>} - a Promise that resolves to an array of ContentNodes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add the typedefs for PageResult and other Object types in the docs as well?

*/
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
* @param {string} [options.appBarColor] - Color for app bar atop the renderer
* @param {string} [options.textColor] - Color for the text or icon
* @param {string} [options.backdropColor] - Color for modal backdrop
* @param {string} [options.backgroundColor] - Color for modal background
* @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()

/*
* Method to return the current version of Kolibri and hence the API available.
* @return {Promise<string>} - A version string
*/
getVersion()
1 change: 1 addition & 0 deletions docs/frontend_architecture/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Frontend architecture
components
conventions
vuex
HTML5_API
dependencies
unit_testing
frontend_build_pipeline
45 changes: 45 additions & 0 deletions integration_testing/custom-html5-apps/kolibri-html5-api.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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
145 changes: 145 additions & 0 deletions packages/hashi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,148 @@ Step 1: Install Hashi package deps (run from Kolibri root)
Step 2: Build Kolibri and hashi

`yarn run build`


Custom Navigation: Kolibri Namespace Data Flow
-----------------------------------------------

The purpose of the ``kolibri.js`` extension of our HTML5 API is to allow a sandboxed HTML5 app to safely request the main Kolibri application's data.

External/partner product teams can create HTML5 applications that are fully embeddable within Kolibri and can read Kolibri content data, which they otherwise wouldn't be able to access.

When a user has permissions to access a custom channel, and they click on it in the main learn tab, rather than viewing "normal Kolibri," they will experience a full-screen HTML5 app. One "out-of-the-box" user interaction is the "navigateTo()" function, which opens a modal that displays a content node. For other data fetching requests, the app, not Kolibri, has the responsibilty of determining what to do with that data.

The HTML5 app uses ``window.kolibri.[function]`` within the app to access the API.

The API is defined in `kolibri.js` and works through a promise chains, to manage async communication, and the postMessageAPI, to facilitate communication between current and parent windows.

The following example will show the complete data flow from the API function, through all of the postMessages, to the data being returned.

Consider an app that is requesting data from a particular content node with
`window.kolibri.getContentById('id12345')`, as defined in `kolibri.js`

```
getContentById(id) {
return self.mediator.sendMessageAwaitReply({
event: events.DATAREQUESTED,
data: { id, dataType: DataTypes.MODEL },
nameSpace,
});
}
```

The messages are managed through the function `sendMessageAwaitReply()` that is defined in `mediator.js`. It is used each time a ``window.kolibri.[function]`` is called, and waits for the data to either be returned or for an error, before resolving and returning data to the original function.

```
sendMessageAwaitReply({ event, data, nameSpace }) {
return new Promise((resolve, reject) => {
const msgId = uuidv4();
let self = this;
function handler(message) {
if (message.message_id === msgId && message.type === 'response') {
if (message.status == MessageStatuses.SUCCESS) {
resolve(message.data);
} else if (message.status === MessageStatuses.FAILURE && message.err) {
reject(message.err);
} else {
// Otherwise something unspecified happened
reject();
}
try {
self.removeMessageHandler({
nameSpace,
event: events.DATARETURNED,
callback: handler,
});
} catch (e) {
// eslint-disable-next-line no-console
console.log(e);
}
}
}
this.registerMessageHandler({
nameSpace,
event: events.DATARETURNED,
callback: handler,
});
data.message_id = msgId;
this.sendMessage({ event, data, nameSpace });
});
}
```

`sendMessageAwaitReply()` calls another function that is defined in the mediator, `sendMessage()` which sends a message to the parent window.



In `mainClient.js`, there are listeners registered for all of the core event types for the kolibri HTML5 API: data requested, navigate to, content, and data returned. On the event, the mediator then sends another message, that also contains the namespace, event, and message.

```
this.on(this.events.DATAREQUESTED, message => {
let event;
if (message.dataType === DataTypes.COLLECTION) {
event = events.COLLECTIONREQUESTED;
} else if (message.dataType === DataTypes.MODEL) {
event = events.MODELREQUESTED;
} else if (message.dataType === DataTypes.SEARCHRESULT) {
event = events.SEARCHRESULTREQUESTED;
} else if (message.dataType === DataTypes.KOLIBRIVERSION) {
event = events.KOLIBRIVERSIONREQUESTED;
}

if (event) {
this.mediator.sendLocalMessage({
nameSpace,
event,
data: message,
});
}
});
```

The listeners for "outgoing" messages are all in `CustomContentRenderer.vue`, the Vue component that renders the full screen view of the HTML5 App and manages requests to the kolibri backend.

```
this.hashi.on(events.MODELREQUESTED, message => {
this.fetchContentModel.call(this, message);
});
```

Here, when the event is omitted, the component uses existing kolibri helper functions like `ContentNodeResource.fetchModel()` to request the data.

```
fetchContentModel(message) {
return ContentNodeResource.fetchModel({ id: message.id })
.then(contentNode => {
return createReturnMsg({ message, data: contentNode });
})
.catch(err => {
return createReturnMsg({ message, err });
})
.then(newMsg => {
this.hashi.mediator.sendMessage(newMsg);
});
},
```

Once there is a response, a return message is created, either with data or with an error, using the `createReturnMsg()` helper function.

```
function createReturnMsg({ message, data, err }) {
// Infer status from data or err
const status = data ? MessageStatuses.SUCCESS : MessageStatuses.FAILURE;
return {
nameSpace: 'hashi',
event: events.DATARETURNED,
data: {
message_id: message.message_id,
type: 'response',
data: data || null,
err: err || null,
status,
},
};
}
```

Finally, the same process of postMessages then happens in reverse, with `CustomContentRenderer.vue` sending a message to `mainClient.js`, which in turn sends a message to `mediator.js` which then resolves or rejects the promise that has been pending with `kolibri.getContentById()`.