kalisio/kano

View on GitHub
src/utils.js

Summary

Maintainability
A
0 mins
Test Coverage
import _ from 'lodash'
import logger from 'loglevel'
import postRobot from 'post-robot'
import { Store } from '@kalisio/kdk/core.client'
import { Router } from './router'

function getEmbedComponent (route) {
  return _.get(route, 'instances.default')
}

function getEmbedRoute (route) {
  // The route component information is only available in the matched ones,
  // and is the last one to be matched in hierarchy
  const matched = _.get(route, 'matched', [])
  return (matched.length ? matched[matched.length - 1] : null)
}

async function callEmbedMethod (route, data) {
  // The event payload contains the name of the method to be called as well as its arguments
  const method = (data ? data.command : undefined)
  let result
  if (method) {
    const component = getEmbedComponent(route)
    if (component && (typeof _.get(component, method) === 'function')) {
      result = (Array.isArray(data.args)
        ? await _.get(component, method)(...data.args)
        : await _.get(component, method)(data.args))
    }
  }
  return result
}

function getEmbedProperty (route, data) {
  // The event payload contains the name of the property to be retrieved
  const property = (data ? data.property : undefined)
  let result
  if (property) {
    const component = getEmbedComponent(route)
    if (component && _.has(component, property)) {
      result = _.get(component, property)
    }
  }
  return result
}

// Setup post-robot event listenr to call component methods on this route from an external domain
// If an event is received but the current route is not the same as the event name the new route is pushed first
function setupEmbedApi (routeName, component) {
  const listener = async (event) => {
    const router = Router.get()
    let route = getEmbedRoute(router.currentRoute.value)
    const data = event.data
    let result, component, interval
    // If event received but the current route does not match the new route is pushed first
    if (route.name !== routeName) {
      // Take care that we might be on a sub-route so that a reset to the parent route is not relevent
      // eg feature edition on the map (see https://github.com/kalisio/kano/issues/339)
      const childRoute = _.find(_.get(router.currentRoute.value, 'matched', []), { name: routeName })
      if (!childRoute) {
        await new Promise((resolve, reject) => router.push({ name: routeName, query: Object.assign({}, route.query) }, resolve))
        // Need to wait until route has really changed, component has been initialized, etc.
        // FIXME: make this more reliable than relying on a timer
        await new Promise((resolve, reject) => {
          interval = setInterval(() => {
            route = getEmbedRoute(router.currentRoute.value)
            component = getEmbedComponent(route)
            if (component) resolve()
          }, 100)
        })
        clearInterval(interval)
      } else {
        // In this case, the component we target is actually the one of the parent route, not the child one
        route = childRoute
      }
    }
    // If no payload this was just a route change
    if (data) {
      result = (data.command ? await callEmbedMethod(route, data) : getEmbedProperty(route, data))
    }
    return result
  }
  // Listen to an event named according to current route name
  postRobot.on(routeName, listener)
  // This is for backward compatibility because at some point we have changed route naming
  // in order to make activities configurable (see https://github.com/kalisio/kano/issues/154)
  // eg 'map' => 'map-activity'
  if (routeName.endsWith('-activity')) postRobot.on(routeName.replace('-activity', ''), listener)
}

async function sendEmbedEvent (...args) {
  // Will fail if not integrated as iframe so check
  if (window.parent !== window) {
    // If no listener post-robot raises an error
    try {
      await postRobot.send(window.parent, ...args)
    } catch (error) {
      logger.debug(error.message)
    }
  }
}

// Build vue router config from our config file
function buildRoutes (config) {
  function buildRoutesRecursively (config, routes, parentRoute) {
    _.forOwn(config, (value, key) => {
      // The key is always the path for the route
      const route = {
        path: key,
        name: key,
        // "Inherit" meta data on nested routes
        meta: (parentRoute ? Object.assign({}, parentRoute.meta) : {})
      }
      // If value is a simple string this is a shortcut:
      // - name = path
      // - component = value
      // Otherwise we have an object similar to what expect vue-router,
      // we simply return the async component loading function with the given component value
      if (typeof value === 'string') {
        route.component = () => import(`@components/${value}.vue`)
      } else {
        // Take care that path can be empty so we cannot just check with a if
        if (_.has(value, 'path')) {
          route.path = value.path
        }
        // Take care that name can be empty so we cannot just check with a if
        if (_.has(value, 'name')) {
          route.name = value.name
        }
        if (_.has(value, 'component')) {
          route.component = () => import(`@components/${value.component}.vue`)
          if (_.has(value, 'embedApi')) {
            setupEmbedApi(route.name)
          }
        }
        if (_.has(value, 'props')) {
          route.props = value.props
        }
        if (_.has(value, 'meta')) {
          // Override parent meta if child meta given
          Object.assign(route.meta, value.meta)
        }
        if (_.has(value, 'redirect')) {
          _.set(route, 'redirect', value.redirect)
        }
      }

      // Check for any children to recurse
      if (value.children) {
        route.children = []
        buildRoutesRecursively(value.children, route.children, route)
      }
      routes.push(route)
    })
  }

  const routes = []
  buildRoutesRecursively(config, routes)
  return routes
}

function buildTours (config) {
  function buildToursRecursively (config, tours) {
    _.forOwn(config, (value, key) => {
      const name = _.get(value, 'name', _.get(value, 'path', key))
      const tour = _.get(value, 'tour')
      if (tour) {
        // If we directly have a tour as an array of steps
        if (Array.isArray(tour)) tours[name] = tour
        // Or a set of tours as key/value object when eg the route has a parameter and each value has its own tour
        // or when the tour is split over multiple linked smaller tours because it is too much complex for a single one
        else if (typeof tour === 'object') {
          _.forOwn(tour, (paramTour, paramValue) => {
            // We identify the main route tour if the key is the same
            if (paramValue === name) tours[`${name}`] = paramTour
            else tours[`${name}/${paramValue}`] = paramTour
          })
        }
      }
      // Check for any children to recurse
      if (value.children) {
        buildToursRecursively(value.children, tours)
      }
    })
  }

  const tours = {}
  buildToursRecursively(config, tours)
  return tours
}

const utils = {
  sendEmbedEvent,
  buildRoutes,
  buildTours
}

export default utils