import _ from 'lodash'
import app from '../../../core/app'
import events from '../../../core/events'
import tools from '../../../core/tools'
import bookLoader from './book/loader/index'
import bookStructureHelper from './book/structure'
import assessments from './assessments/index'
import { getMetadata } from './guides-loader/index'
import helpers from './helpers'
import guidesEvents from './events'
import { BOOK_TYPES } from './consts'
import { getSectionLayoutName, prepareFormData } from './settings/layout/data'
const EVENTS = {
goToSection: 'guides:section:goto_id',
editAssessment: 'guides:assessment:edit',
getGuidesPageInfo: 'guides:player:getPageInfo',
bookAddChapter: 'guides:book:add_chapter',
bookAddSection: 'guides:book:add_section',
bookAddPage: 'guides:book:add_page',
bookRemoveItem: 'guides:book:remove_item',
bookRenameItem: 'guides:book:rename_item',
bookLinkPage: 'guides:book:link_page'
}
const emit = events.create(EVENTS)
const callbacks = {
guides: {
onCommandExecute: {},
onCommandResult: {},
assessments: {
onExecute: {},
onResult: {}
}
}
}
const convertActionToInternal = (action) => {
const baseFields = { action: 'open', panel: action.panel }
switch (action.type) {
case window.codioIDE.guides.structure.ACTION_TYPE.FILE:
return { ...baseFields, path: action.fileName }
case window.codioIDE.guides.structure.ACTION_TYPE.PREVIEW:
return { ...baseFields, path: `#preview: ${action.url}` }
case window.codioIDE.guides.structure.ACTION_TYPE.TERMINAL:
return { ...baseFields, path: `#terminal: ${action.command}` }
case window.codioIDE.guides.structure.ACTION_TYPE.HIGHLIGHT:
return {
...baseFields,
path: action.fileName,
ref: action.reference,
lineCount: action.lines
}
case window.codioIDE.guides.structure.ACTION_TYPE.VISUALIZER:
return {
...baseFields,
path: `#tutor: ${action.fileName}`
}
case window.codioIDE.guides.structure.ACTION_TYPE.VM:
return { ...baseFields, path: `#vm: ${action.fileName}` }
case window.codioIDE.guides.structure.ACTION_TYPE.VM_TERMINAL:
return { ...baseFields, path: `#vmssh: ${action.command}` }
case window.codioIDE.guides.structure.ACTION_TYPE.EARSKETCH:
return { ...baseFields, path: `#earsketch: ${action.fileName}` }
case window.codioIDE.guides.structure.ACTION_TYPE.JUPYTER_LAB:
return { ...baseFields, path: `#jupyter-lab: ${action.fileName}` }
}
}
const convertMediaActionToInternal = (action) => {
const baseFields = {
type: action.type,
time: action.time,
content: action.fileNameOrCommand || '',
lines: _.isNumber(action.lines) ? action.lines : null
}
switch (action.type) {
case window.codioIDE.guides.structure.MEDIA_ACTION_TYPE.FILE_OPEN:
return {
...baseFields,
action: 'open',
tabType: 'file',
panel: action.panel
}
case window.codioIDE.guides.structure.MEDIA_ACTION_TYPE.FILE_CLOSE:
return {
...baseFields,
action: 'close',
tabType: 'file',
panel: action.panel
}
case window.codioIDE.guides.structure.MEDIA_ACTION_TYPE.TERMINAL_OPEN:
return {
...baseFields,
action: 'open',
tabType: 'terminal',
panel: action.panel
}
case window.codioIDE.guides.structure.MEDIA_ACTION_TYPE.TERMINAL_CLOSE:
return {
...baseFields,
action: 'close',
tabType: 'terminal',
panel: action.panel
}
case window.codioIDE.guides.structure.MEDIA_ACTION_TYPE.RUN_COMMAND:
return {
...baseFields,
action: 'open',
tabType: 'terminal',
panel: action.panel
}
case window.codioIDE.guides.structure.MEDIA_ACTION_TYPE.HIGHLIGHT:
return {
...baseFields,
action: 'open',
tabType: 'file',
panel: action.panel,
reference: action.reference
}
default:
return baseFields
}
}
const convertInternalActionToOptions = (action) => {
if (action.action === 'close') {
return
}
let type
const data = {}
if (action.path.indexOf('#preview: ') === 0) {
type = window.codioIDE.guides.structure.ACTION_TYPE.PREVIEW
data.url = action.path.replace('#preview: ', '')
} else if (action.path.indexOf('#terminal: ') === 0) {
type = window.codioIDE.guides.structure.ACTION_TYPE.TERMINAL
data.command = action.path.replace('#terminal: ', '')
} else if (action.path.indexOf('#tutor: ') === 0) {
type = window.codioIDE.guides.structure.ACTION_TYPE.VISUALIZER
data.fileName = action.path.replace('#tutor: ', '')
} else if (action.path.indexOf('#vm: ') === 0) {
type = window.codioIDE.guides.structure.ACTION_TYPE.VM
data.fileName = action.path.replace('#vm: ', '')
} else if (action.path.indexOf('#vmssh: ') === 0) {
type = window.codioIDE.guides.structure.ACTION_TYPE.VM_TERMINAL
data.fileName = action.path.replace('#vmssh: ', '')
} else if (action.path.indexOf('#earsketch: ') === 0) {
type = window.codioIDE.guides.structure.ACTION_TYPE.EARSKETCH
data.fileName = action.path.replace('#earsketch: ', '')
} else if (action.path.indexOf('#jupyter-lab: ') === 0) {
type = window.codioIDE.guides.structure.ACTION_TYPE.JUPYTER_LAB
data.fileName = action.path.replace('#jupyter-lab: ', '')
} else if (!_.isUndefined(action.ref)) {
type = window.codioIDE.guides.structure.ACTION_TYPE.HIGHLIGHT
data.fileName = action.path
data.reference = action.ref
data.lines = action.lineCount
} else {
type = window.codioIDE.guides.structure.ACTION_TYPE.FILE
data.fileName = action.path
}
return {
type,
panel: action.panel,
...data
}
}
const convertInternalMediaActionToOptions = (action) => {
const data = {}
if (action.action === 'open' || action.action === 'close') {
data.panel = action.panel
data.fileNameOrCommand = action.content || ''
}
if (action.type === 'highlight') {
data.reference = action.reference
data.lines = action.lines
}
return {
type: action.type,
time: action.time,
...data
}
}
const convertInternalSectionToOptions = async (metadata, section) => {
if (!section) {
return null
}
const layoutSettings = prepareFormData(metadata, section)
const content = await window.codioIDE.files.getContent(section.contentPath)
const sectionJson = section.json
return {
title: sectionJson.title,
layout: layoutSettings.layout,
guidesOnLeft: layoutSettings.guidesOnLeft,
actions:
sectionJson.files
?.map(convertInternalActionToOptions)
.filter((action) => action) || [],
content,
showFileTree: layoutSettings.showFileTree,
showFolders: sectionJson.path,
closeAllTabs: section.isCloseAll,
closeTerminalSession: sectionJson.closeTerminalSession,
teacherOnly: sectionJson.teacherOnly,
learningObjectives: sectionJson.learningObjectives,
media: sectionJson.media
? {
...sectionJson.media,
actions:
sectionJson.media.actions?.map(
convertInternalMediaActionToOptions
) || []
}
: undefined
}
}
const convertStructureOptionsToInternal = (options, currentOptions = {}) => {
const finalOptions = _.merge({ ...currentOptions }, { ...options })
const actions =
finalOptions.actions?.map((action) => convertActionToInternal(action)) || []
if (finalOptions.closeAllTabs) {
actions.unshift({ path: '#tabs', action: 'close' })
}
const layout = getSectionLayoutName(
finalOptions.layout,
finalOptions.showFileTree,
finalOptions.guidesOnLeft
)
return {
type: finalOptions.type,
title: finalOptions.title,
value: finalOptions.content,
files: actions,
layout: layout,
path: finalOptions.showFileTree ? finalOptions.showFolders : [],
teacherOnly: finalOptions.teacherOnly,
closeTerminalSession: finalOptions.closeTerminalSession,
learningObjectives: finalOptions.learningObjectives,
media: finalOptions.media
? {
type: 'audio',
name: finalOptions.media.name,
source: finalOptions.media.source,
disabled: !!finalOptions.media.disabled,
actions: finalOptions.media.actions?.map(convertMediaActionToInternal)
}
: undefined
}
}
const activateTab = async (tabName) => {
const tab = app.ide.panels.getOpenedTabs(tabName)[0]
if (!tab) {
throw new Error('tab not found')
}
if (tab && !tab.active) {
return app.ide.panels.open(tabName)
}
}
const checkEditorOpened = async () => {
try {
await activateTab('guides-edit')
} catch {
throw new Error('guides editor should be opened')
}
}
/**
* Codio IDE API object
* @exports window.codioIDE
* @type {object}
* @namespace codioIDE
* @global
*/
window.codioIDE = window.codioIDE || {}
/**
* Guides API methods
* @memberof codioIDE
* @type {object}
* @namespace codioIDE.guides
*/
window.codioIDE.guides = {
/**
* Returns guides metadata object
* @memberof codioIDE.guides
* @async
* @method getMetadata
*/
getMetadata: async () => {
if (!app.currentProject) {
throw new Error('project not found')
}
return getMetadata()
},
/**
* Returns unused guides metadata info
* @memberof codioIDE.guides
* @async
* @method getUnusedMetadataInfo
*/
getUnusedMetadataInfo: async () => {
const metadata = await getMetadata()
return metadata.getUnusedMetadataInfo()
},
/**
* Recalculates guides metadata caches
* @memberof codioIDE.guides
* @async
* @method refreshMetadataCaches
*/
refreshMetadataCaches: async () => {
const metadata = await getMetadata()
return metadata.refreshMetadataCaches()
},
/**
* IDE guides structure API methods
* @memberof codioIDE.guides
* @type {object}
* @namespace codioIDE.guides.structure
*/
structure: {
/**
* @typedef codioIDE.guides.structure.GuidesStructureItem
* @memberof codioIDE.guides.structure
* @type {Object}
* @property {string} id - item id
* @property {string} title - title
* @property {string} [pageId] - page id if item has content
* @property {codioIDE.structure.ITEM_TYPES} type - item type
* @property {codioIDE.guides.structure.GuidesStructureItem} [children] - children if item is a sections or chapter
*/
/**
* @typedef codioIDE.guides.structure.GuidesStructure
* @memberof codioIDE.guides.structure
* @type {Object}
* @property {string} name - name, obsolete
* @property {codioIDE.guides.structure.GuidesStructureItem} children - guides children(chapters, sections, pages)
*/
/**
* @typedef codioIDE.guides.structure.ActionBase
* @memberof codioIDE.guides.structure
* @type {Object}
* @property {codioIDE.guides.structure.ACTION_TYPE} type - action id
* @property {number} panel - action panel
*/
/**
* @typedef {codioIDE.guides.structure.ActionBase} codioIDE.guides.structure.ActionWithFile
* @memberof codioIDE.guides.structure
* @extends codioIDE.guides.structure.ActionBase
* @type {Object}
* @property {string} fileName - file name
*/
/**
* @typedef {codioIDE.guides.structure.ActionBase} codioIDE.guides.structure.ActionPreview
* @memberof codioIDE.guides.structure
* @extends codioIDE.guides.structure.ActionBase
* @type {Object}
* @property {string} url - url
*/
/**
* @typedef {codioIDE.guides.structure.ActionWithFile} codioIDE.guides.structure.ActionHighlight
* @memberof codioIDE.guides.structure
* @extends codioIDE.guides.structure.ActionWithFile
* @type {Object}
* @property {string} reference - reference
* @property {string} [lines] - lines count
*/
/**
* @typedef {codioIDE.guides.structure.ActionBase} codioIDE.guides.structure.ActionTerminal
* @memberof codioIDE.guides.structure
* @extends codioIDE.guides.structure.ActionBase
* @type {Object}
* @property {string} command - command
*/
/**
* @typedef codioIDE.guides.structure.MediaActionBase
* @memberof codioIDE.guides.structure
* @type {Object}
* @property {codioIDE.guides.structure.MEDIA_ACTION_TYPE} type - action type
* @property {number} time - time in seconds
*/
/**
* @typedef {codioIDE.guides.structure.MediaActionBase} codioIDE.guides.structure.MediaTabAction
* @memberof codioIDE.guides.structure
* @extends codioIDE.guides.structure.MediaActionBase
* @type {Object}
* @property {string} fileNameOrCommand - filename or command for terminal
* @property {number} panel - panel number
*/
/**
* @typedef {codioIDE.guides.structure.MediaTabAction} codioIDE.guides.structure.MediaActionHighlight
* @memberof codioIDE.guides.structure
* @extends codioIDE.guides.structure.MediaTabAction
* @type {Object}
* @property {string} reference - reference
* @property {number} lines - lines count
*/
/**
* @typedef codioIDE.guides.structure.MediaOptions
* @memberof codioIDE.guides.structure
* @type {Object}
* @property {string} name - name
* @property {string} source - fileName, should exist into .guides/media folder
* @property {Array.<(codioIDE.guides.structure.MediaTabAction|codioIDE.guides.structure.MediaActionBase|codioIDE.guides.structure.MediaActionHighlight)>} actions - actions
* @property {boolean} [disabled] - disable media
*/
/**
* @typedef codioIDE.guides.structure.UpdateOptions
* @memberof codioIDE.guides.structure
* @type {Object}
* @property {string} [title] - title
* @property {codioIDE.guides.structure.LAYOUT} [layout] - layout
* @property {boolean} [guidesOnLeft] - guides on left
* @property {string} [content] - content
* @property {Array.<(codioIDE.guides.structure.ActionBase|codioIDE.guides.structure.ActionWithFile|codioIDE.guides.structure.ActionPreview|codioIDE.guides.structure.ActionHighlight|codioIDE.guides.structure.ActionTerminal)>} [actions] - page actions that will be executed on page open
* @property {boolean} [showFileTree] - show file tree
* @property {string[]} [showFolders] - paths, show only the selected folders in filetree
* @property {boolean} [closeAllTabs] - close terminal session on page open
* @property {boolean} [closeTerminalSession] - close terminal session on page open
* @property {boolean} [teacherOnly] - if true - item will be visible only for teachers
* @property {boolean} [learningObjectives] - learning objectives, for items with content
* @property {codioIDE.guides.structure.MediaOptions} [media] - item media settings, for items with content
*/
/**
* @typedef {codioIDE.guides.structure.UpdateOptions} codioIDE.guides.structure.AddOptions
* @extends codioIDE.guides.structure.UpdateOptions
* @memberof codioIDE.guides.structure
* @type {Object}
* @property {codioIDE.guides.structure.ITEM_TYPES} type - item type
* @property {string} title - title
*/
/**
* @typedef codioIDE.guides.structure.GetResult
* @memberOf codioIDE.guides.structure
* @type {Object}
* @property {codioIDE.guides.structure.GuidesStructureItem} structure - guides structure item
* @property {codioIDE.guides.structure.UpdateOptions|null} settings - guides section data
*/
/**
* Returns guides book structure
* @memberof codioIDE.guides.structure
* @async
* @method getStructure
* @return {Promise<codioIDE.guides.structure.GuidesStructure>}
* @throws Will throw an error if the project is not found
* @example
* const structure = await codioIDE.guides.structure.getStructure()
* // return guides structure object: {name: '...', children: [{id: '...', title: '...', type: '...', children: [...]}]}
*/
getStructure: async () => {
if (!app.currentProject) {
throw new Error('project not found')
}
return bookLoader.getBookStructure()
},
/**
* Add item to the guides book structure
* @memberof codioIDE.guides.structure
* @async
* @method add
* @param {codioIDE.guides.structure.AddOptions} settings - item settings
* @param {string} [parent] - parent item id
* @param {number} [index] - index to insert
* @throws Will throw an error if the project is not found or guides is not opened
* @return {Promise<codioIDE.guides.structure.GuidesStructureItem>}
* @example
* try {
* const res = await window.codioIDE.guides.structure.add({type: window.codioIDE.guides.structure.ITEM_TYPES.PAGE, value: 'new content'}, null, 2)
* console.log('add item result', res) // returns added item: {id: '...', title: '...', type: '...', children: [...]}
* } catch (e) {
* console.error(e)
* }
*/
add: async (settings, parent, index = Number.MAX_SAFE_INTEGER) => {
if (!app.currentProject) {
throw new Error('project not found')
}
if (!settings?.type) {
throw new Error('settings should have type property')
}
if (!settings?.title) {
throw new Error('settings should have title property')
}
await checkEditorOpened()
const structure = await bookLoader.getBookStructure()
if (settings.type === BOOK_TYPES.CHAPTER && parent) {
throw new Error('parent should be null for chapter')
}
let parentNode = parent
? bookStructureHelper.searchItem(parent, structure)
: null
if (
settings.type === BOOK_TYPES.SECTION &&
parentNode &&
parentNode.type !== BOOK_TYPES.CHAPTER
) {
throw new Error('parent should be null or a chapter')
}
if (
settings.type === BOOK_TYPES.PAGE &&
parentNode?.type === BOOK_TYPES.PAGE
) {
throw new Error('parent should be a chapter or section')
}
if (settings.content && !settings.layout) {
throw new Error('layout is required for items with content')
}
const d = tools.promise.deferred()
parentNode = parentNode || structure
const { children } = parentNode
let insertBefore = index >= 0 && index < children.length
const data = {
parentId: parent,
insertInto: index,
options: convertStructureOptionsToInternal(settings),
insertBefore,
callback: d.resolve,
errorCallback: d.reject
}
if (settings.type === BOOK_TYPES.CHAPTER) {
emit.bookAddChapter(data)
} else if (settings.type === BOOK_TYPES.SECTION) {
emit.bookAddSection(data)
} else {
emit.bookAddPage(data)
}
return d.promise
},
/**
* Update item in the guides book structure
* @memberof codioIDE.guides.structure
* @async
* @method update
* @throws Will throw an error if the project is not found, guides is not opened, id is not a string, settings is not an object
* @param {string} id - item id
* @param {codioIDE.guides.structure.UpdateOptions} settings - item settings
* @return {Promise<void>}
* @example
* try {
* await window.codioIDE.guides.structure.update('nodeId', {
* title: 'new title'
* })
* console.log('item updated')
* } catch (e) {
* console.error(e)
* }
*/
update: async (id, settings) => {
if (!app.currentProject) {
throw new Error('project not found')
}
if (!id) {
throw new Error('id should be a string')
}
if (!settings) {
throw new Error('settings should be an object')
}
if (settings.type) {
throw new Error('type should not be updated')
}
await checkEditorOpened()
const structureP = bookLoader.getBookStructure()
const metadataP = getMetadata()
const [structure, metadata] = await Promise.all([structureP, metadataP])
const node = bookStructureHelper.searchItem(id, structure)
if (!node) {
throw new Error('item not found')
}
const section = metadata.getSectionById(node.pageId)
const currentSettings = (await convertInternalSectionToOptions(
metadata,
section
)) || { title: node.title }
const internalOptions = convertStructureOptionsToInternal(
settings,
currentSettings
)
const { value, type, ...rest } = internalOptions
if (settings.title && settings.title !== currentSettings.title) {
const renameD = tools.promise.deferred()
emit.bookRenameItem({
id,
title: settings.title,
callback: renameD.resolve,
errorCallback: renameD.reject
})
await renameD.promise
}
if (!node.pageId) {
const linkD = tools.promise.deferred()
emit.bookLinkPage({
id,
link: true,
options: { value, ...rest },
callback: linkD.resolve,
errorCallback: linkD.reject
})
return linkD.promise
}
const {
title: titleSettings,
content: contentSettings,
...restSettings
} = settings
if (_.isUndefined(contentSettings) && _.isEmpty(restSettings)) {
return
}
if (value) {
await section.setValue(value)
}
await section.update({ ...rest })
return section.save()
},
/**
* Delete items from the guides book structure
* @memberof codioIDE.guides.structure
* @async
* @method delete
* @param {string[]} ids - item ids
* @throws Will throw an error if the project is not found or guides is not opened
* @return {Promise<void>}
* @example
* try {
* await window.codioIDE.guides.structure.delete(['nodeId'])
* console.log('remove items done')
* } catch (e) {
* console.error(e)
* }
*/
delete: async (ids) => {
if (!app.currentProject) {
throw new Error('project not found')
}
if (!_.isArray(ids)) {
throw new Error('ids should be an array')
}
await checkEditorOpened()
const structure = await bookLoader.getBookStructure()
ids = ids.filter((id) => bookStructureHelper.searchItem(id, structure))
if (!ids.length) {
return
}
const d = tools.promise.deferred()
emit.bookRemoveItem({ ids, callback: d.resolve, errorCallback: d.reject })
return d.promise
},
/**
* Get item
* @param {string} id - item id
* @return {Promise<codioIDE.guides.structure.GetResult>}
* @example
* try {
* const res = await window.codioIDE.guides.structure.get('nodeId')
* console.log('res', res)
* } catch (e) {
* console.error(e)
* }
*/
get: async (id) => {
if (!app.currentProject) {
throw new Error('project not found')
}
if (!id) {
throw new Error('id should be a string')
}
const structureP = bookLoader.getBookStructure()
const metadataP = getMetadata()
const [structure, metadata] = await Promise.all([structureP, metadataP])
const node = bookStructureHelper.searchItem(id, structure)
if (!node) {
throw new Error('item not found')
}
const section = node.pageId ? metadata.getSectionById(node.pageId) : null
const settings = await convertInternalSectionToOptions(metadata, section)
return { structure: node, settings }
},
/**
* @memberof codioIDE.guides.structure
* @readonly
* @enum {string}
*/
ITEM_TYPES: {
CHAPTER: BOOK_TYPES.CHAPTER,
SECTION: BOOK_TYPES.SECTION,
PAGE: BOOK_TYPES.PAGE
},
/**
* @memberof codioIDE.guides.structure
* @readonly
* @enum {string}
*/
ACTION_TYPE: {
FILE: 'file',
PREVIEW: 'preview',
TERMINAL: 'terminal',
HIGHLIGHT: 'highlight',
VISUALIZER: 'visualizer',
VM: 'vm',
VM_TERMINAL: 'vm_terminal',
EARSKETCH: 'earsketch',
JUPYTER_LAB: 'jupyter_lab'
},
/**
* @memberof codioIDE.guides.structure
* @readonly
* @enum {string}
*/
MEDIA_ACTION_TYPE: {
FILE_OPEN: 'file_open',
FILE_CLOSE: 'file_close',
TERMINAL_OPEN: 'terminal_open',
TERMINAL_CLOSE: 'terminal_close',
RUN_COMMAND: 'run_command',
HIGHLIGHT: 'highlight',
CLOSE_ALL: 'close_all',
PAUSE: 'pause'
},
/**
* @memberof codioIDE.guides.structure
* @readonly
* @enum {string}
*/
LAYOUT: {
L_1_PANEL: '1-panel',
L_2_PANELS: '2-panels',
L_3_COLUMNS: '3-columns',
L_3_CELL: '3-cell',
L_4_CELL: '4-cell'
}
},
openEditor: (step) => {
const params = step !== undefined ? { step } : undefined
guidesEvents.edit(null, params)
},
isEditorOpen: guidesEvents.isEditorOpen,
goToSection: (data) => {
// sectionTitle, sectionId, optionalAnchor
emit.goToSection(data)
},
getGuidesPageInfo: async () => {
const guidesTab = app.ide.panels.getOpenedTabs('guides')[0]
if (!guidesTab) {
return { content: '', name: '' }
}
return new Promise((resolve) => {
emit.getGuidesPageInfo((info) => resolve(info))
})
},
/**
* Command execute callback
* @memberof codioIDE.guides
* @callback CommandExecuteCallback
* @param {string} command - command
*/
/**
* Register an events listener
* An event emitted when a command started executing
* @param {CommandExecuteCallback} callback - command execute callback
* @throws Will throw an error if the callback is not a function.
* @return {callback} - call it to deregister the event
*/
onCommandExecute: (callback) => {
if (!_.isFunction(callback)) {
throw new Error('Callback should be a function')
}
if (!window.codioIDE.guides.onCommandExecute._handler) {
window.codioIDE.guides.onCommandExecute._handler = events.subscribe(
'guides:command:onExecute',
(topic, data) => {
_.forEach(callbacks.guides.onCommandExecute, (callback) =>
callback(data.cmd)
)
}
)
}
const id = crypto.randomUUID()
callbacks.guides.onCommandExecute[id] = callback
return () => {
delete callbacks.guides.onCommandExecute[id]
if (
window.codioIDE.guides.onCommandExecute._handler &&
_.isEmpty(callbacks.guides.onCommandExecute)
) {
events.unsubscribe(window.codioIDE.guides.onCommandExecute._handler)
}
}
},
/**
* Command result callback
* @memberof codioIDE.guides
* @callback CommandResultCallback
* @param {string} command - command
* @param {string} output - command output
* @param {number} format - output format(1 - text, 2 - markdown, 3 - html)
* @param {string} err - error
*/
/**
* Register an events listener
* An event emitted when a command has executed
* @param {CommandResultCallback} callback - command result callback
* @throws Will throw an error if the callback is not a function.
* @return {callback} - call it to deregister the event
*/
onCommandResult: (callback) => {
if (!_.isFunction(callback)) {
throw new Error('Callback should be a function')
}
if (!window.codioIDE.guides.onCommandResult._handler) {
window.codioIDE.guides.onCommandResult._handler = events.subscribe(
'guides:command:onResult',
(topic, { command, output, err }) => {
_.forEach(callbacks.guides.onCommandResult, (callback) =>
callback(command, output, err)
)
}
)
}
const id = crypto.randomUUID()
callbacks.guides.onCommandResult[id] = callback
return () => {
delete callbacks.guides.onCommandResult[id]
if (
window.codioIDE.guides.onCommandResult._handler &&
_.isEmpty(callbacks.guides.onCommandResult)
) {
events.unsubscribe(window.codioIDE.guides.onCommandResult._handler)
}
}
}
}
/**
* Guides assessments API methods
* @memberof codioIDE.guides
* @type {object}
* @namespace codioIDE.guides.assessments
*/
window.codioIDE.guides.assessments = {
/**
* Returns guides assessments
* @memberof codioIDE.guides.assessments
* @async
* @method list
*/
list: async () => {
if (!app.currentProject) {
throw new Error('project not found')
}
return assessments.assessments.getAssessments()
},
findIds: (content, contentType) => {
return helpers.findAssessmentsIds(content, contentType)
},
openEditor: (assessment) => {
emit.editAssessment(assessment)
},
/**
* Assessment execute callback
* @memberof codioIDE.guides.assessments
* @callback AssessmentExecuteCallback
* @param {string} assessmentId - assessment id
*/
/**
* Register an events listener
* An event emitted when an assignment started executing
* @param {AssessmentExecuteCallback} callback - assessment execute callback
* @throws Will throw an error if the callback is not a function.
* @return {callback} - call it to deregister the event
*/
onExecute: (callback) => {
if (!_.isFunction(callback)) {
throw new Error('Callback should be a function')
}
if (!window.codioIDE.guides.assessments.onExecute._handler) {
window.codioIDE.guides.assessments.onExecute._handler = events.subscribe(
'guides:assessments:onExecute',
(topic, assessmentId) => {
_.forEach(callbacks.guides.assessments.onExecute, (callback) =>
callback(assessmentId)
)
}
)
}
const id = crypto.randomUUID()
callbacks.guides.assessments.onExecute[id] = callback
return () => {
delete callbacks.guides.assessments.onExecute[id]
if (
window.codioIDE.guides.assessments.onExecute._handler &&
_.isEmpty(callbacks.guides.assessments.onExecute)
) {
events.unsubscribe(
window.codioIDE.guides.assessments.onExecute._handler
)
}
}
},
/**
* Assessment result callback
* @memberof codioIDE.guides.assessments
* @callback AssessmentResultCallback
* @param {string} assessmentId - assessment id
* @param {string} result - execution result
*/
/**
* @typedef codioIDE.guides.assessments.AssessmentResult
* @memberof codioIDE.guides.assessments
* @interface
* @type {Object}
* @property {string} [output] - output
* @property {number} [points] - points
* @property {string} result - assessment result state (fail|success|partial)
* @property {number} [attempt] - user attempt
*/
/**
* Register an events listener
* An event emitted when an assessment has executed
* @param {AssessmentResultCallback} callback - assessment result callback
* @throws Will throw an error if the callback is not a function.
* @return {callback} - call it to deregister the event
*/
onResult: (callback) => {
if (!_.isFunction(callback)) {
throw new Error('Callback should be a function')
}
if (!window.codioIDE.guides.assessments.onResult._handler) {
window.codioIDE.guides.assessments.onResult._handler = events.subscribe(
'guides:assessments:onResult',
(topic, { assessmentId, result }) => {
_.forEach(callbacks.guides.assessments.onResult, (callback) =>
callback(assessmentId, result)
)
}
)
}
const id = crypto.randomUUID()
callbacks.guides.assessments.onResult[id] = callback
return () => {
delete callbacks.guides.assessments.onResult[id]
if (
window.codioIDE.guides.assessments.onResult._handler &&
_.isEmpty(callbacks.guides.assessments.onResult)
) {
events.unsubscribe(window.codioIDE.guides.assessments.onResult._handler)
}
}
}
}