ICIJ/datashare-client

View on GitHub
src/core/Core.js

Summary

Maintainability
A
0 mins
Test Coverage
// BootstrapVue recommends using this
import 'mutationobserver-shim'

import compose from 'lodash/fp/compose'
import Murmur from '@icij/murmur'
import BootstrapVue from 'bootstrap-vue'
import VCalendar from 'v-calendar/lib/v-calendar.umd'
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import VueProgressBar from 'vue-progressbar'
import VueRouter from 'vue-router'
import VueScrollTo from 'vue-scrollto'
import VueShortkey from 'vue-shortkey'
import VueWait from 'vue-wait'
import VueEllipseProgress from 'vue-ellipse-progress'
import { iteratee } from 'lodash'

import ComponentsMixin from './ComponentsMixin'
import FiltersMixin from './FiltersMixin'
import HooksMixin from './HooksMixin'
import I18nMixin from './I18nMixin'
import PipelinesMixin from './PipelinesMixin'
import ProjectsMixin from './ProjectsMixin'
import WidgetsMixin from './WidgetsMixin'

import { dispatch } from '@/utils/event-bus'
import Auth from '@/api/resources/Auth'
import messages from '@/lang/en'
import { getMode, MODE_NAME } from '@/mode'
import router from '@/router'
import guards from '@/router/guards'
import { storeBuilder } from '@/store/storeBuilder'
import settings from '@/utils/settings'

class Base {}
const Behaviors = compose(
  ComponentsMixin,
  FiltersMixin,
  HooksMixin,
  I18nMixin,
  PipelinesMixin,
  ProjectsMixin,
  WidgetsMixin
)(Base)

/**
  @class
  @classdesc Class representing the core application with public methods for plugins.
  @mixes FiltersMixin
  @mixes HooksMixin
  @mixes I18nMixin
  @mixes PipelinesMixin
  @mixes ProjectsMixin
  @mixes WidgetsMixin
  @typicalname datashare
*/
class Core extends Behaviors {
  /**
   * Create an application
   * @param {Object} LocalVue - The Vue class to instantiate the application with.
   * @param api - Datashare api interface
   * @param mode - mode of authentication ('local' or 'server'
   */
  constructor(LocalVue = Vue, api = null, mode = getMode(MODE_NAME.LOCAL)) {
    super(LocalVue)
    this.LocalVue = LocalVue
    this._api = api
    this._store = storeBuilder(api)
    this._auth = new Auth(mode, this._api)
    // Disable production tip when not in production
    this.LocalVue.config.productionTip = import.meta.env.MODE === 'development'
    // Setup deferred state
    this.defer()
  }
  /**
   * Add a Vue plugin to the instance's LocalVue
   * @param {Object} Plugin - The actual Vue plugin class
   * @param {Object} options - Option to pass to the plugin
   * @returns {Core} the current instance of Core
   */
  use(Plugin, options) {
    this.LocalVue.use(Plugin, options)
    return this
  }
  /**
   * Configure all default Vue plugins for this application
   * @returns {Core} the current instance of Core
   */
  useAll() {
    this.useI18n()
    this.useBootstrapVue()
    this.useCommons()
    this.useRouter()
    this.useWait()
    this.useCore()
    return this
  }
  /**
   * Configure vue-i18n plugin
   * @returns {Core} the current instance of Core
   */
  useI18n() {
    this.use(VueI18n)
    this.i18n = new VueI18n({
      locale: settings.defaultLocale,
      fallbackLocale: settings.defaultLocale,
      messages: {
        [settings.defaultLocale]: messages
      }
    })
    return this
  }
  /**
   * Configure bootstrap-vue plugin
   * @returns {Core} the current instance of Core
   */
  useBootstrapVue() {
    this.use(BootstrapVue, {
      BPopover: {
        boundaryPadding: 14
      },
      BTooltip: {
        boundaryPadding: 0
      }
    })
    return this
  }
  /**
   * Configure vue-router plugin
   * @returns {Core} the current instance of Core
   */
  useRouter() {
    this.use(VueRouter)
    this.router = new VueRouter(router)
    guards(this)
    return this
  }
  /**
   * Configure most common Vue plugins (Murmur, VueProgressBar, VueShortkey, VueScrollTo and VueCalendar)
   * @returns {Core} the current instance of Core
   */
  useCommons() {
    // Common plugins
    this.use(Murmur)
    this.use(VueProgressBar, { color: settings.progressBar.color })
    this.use(VueShortkey, { prevent: settings.hotKeyPrevented })
    this.use(VueScrollTo)
    this.use(VueScrollTo)
    this.use(VueEllipseProgress)
    // Setup VCalendar manually since Webpack is not compatible with
    // dynamic chunk import with third party modules.
    // @see https://github.com/nathanreyes/v-calendar/issues/413#issuecomment-530633437
    this.use(VCalendar, { componentPrefix: 'vc' })
    return this
  }
  /**
   * Configure vue-wait plugin
   * @returns {Core} the current instance of Core
   */
  useWait() {
    this.use(VueWait)
    this.wait = new VueWait({ useVuex: true })
    return this
  }
  /**
   * Add a $core property to the instance's Vue
   * @returns {Core} the current instance of Core
   */
  useCore() {
    const core = this
    this.use(
      class VueCore {
        static install(Vue) {
          Vue.prototype.$core = core
        }
      }
    )
    return this
  }
  /**
   * Load settings from the server and instantiate most the application configuration.
   * @async
   * @fullfil {Core} - The instance of the core application
   * @reject {Object} - The Error object
   * @returns {Promise<Object>}
   */
  async configure() {
    try {
      // Override Murmur default value for content-placeholder
      this.config.set('content-placeholder.rows', settings.contentPlaceholder.rows)
      // Get the config object
      await this.loadSettings()
      // Create the default project for the current user or redirect to login
      if (this.mode.modeName !== 'server') {
        if (!(await this.defaultProjectExists())) {
          await this.createDefaultProject()
        }
      }
      this._auth = new Auth(this.mode)
      // Set the default project
      if (!this.store.state.search.indices.length) {
        this.store.commit('search/indices', [this.getDefaultProject()])
      }
      // Check if "Download" functionality is available for the selected project
      // Because otherwise, if the FilterPanel is closed, it is never called
      await this.store.dispatch('downloads/fetchIndicesStatus')
      // Initialize current locale
      await this.initializeI18n()
      // Hold a promise that is resolved when the core is configured
      return this.ready && this._readyResolve(this)
    } catch (error) {
      return this.ready && this._readyReject(error)
    }
  }

  getDefaultProject() {
    const userProjects = this.config.get('projects', []).map(iteratee('name'))
    if (userProjects.length === 0) {
      return ''
    }
    const defaultProject = this.config.get('defaultProject', '')
    return userProjects.indexOf(defaultProject) === -1 ? userProjects[0] : defaultProject
  }

  /**
   * Mount the instance's vue application
   * @param {String} [selector=#app] - Query selector to the mounting point
   * @returns {Vue} The instantiated Vue
   */
  mount(selector = '#app') {
    // Render function returns a router-view component by default
    const render = (h) => h('router-view')
    // We do not necessarily use the default Vue so we can use this function
    // from our unit tests
    const vm = new this.LocalVue({
      render,
      wait: this.wait,
      i18n: this.i18n,
      router: this.router,
      store: this.store
    }).$mount(selector)
    // Return an instance of the Vue constructor we receive.
    return vm
  }
  /**
   * Build a promise to be resolved when the application is configured.
   */
  defer() {
    this._ready = new Promise((resolve, reject) => {
      this._readyResolve = resolve
      this._readyReject = reject
    })
    // Notify the document the core is ready
    this._ready.then(() => this.dispatch('ready'))
  }
  /**
   * Dispatch an event from the document root, passing the core application through event message.
   * @param {String} name - Name of the event to fire
   * @param {...Mixed} args - Additional params to pass to the event
   * @returns {Core} the current instance of Core
   */
  dispatch(name, ...args) {
    dispatch(name, { app: this, core: this, ...args })
    return this
  }
  /**
   * Get the current signed user.
   * @async
   * @fullfil {Object} Current user
   * @returns {Promise<Object>}
   */
  getUser() {
    return this.api.getUser()
  }
  /**
   * Get and update user definitionin place
   * @async
   * @returns {Promise}
   */
  async loadUser() {
    // Load the user
    this.config.merge(await this.getUser())
  }
  /**
   * Get settings (both from the server settings and the current mode)
   * @async
   * @returns {Promise}
   */
  async loadSettings() {
    // Get the config object
    const serverSettings = await this.api.getSettings()
    // Load the user and update the settings accordingly
    await this.loadUser()
    // Murmur exposes a config attribute which shares a Config object
    // with the current vue instance.
    this.config.merge(getMode(serverSettings.mode))
    // The backend can yet override some configuration
    this.config.merge(serverSettings)
  }
  /**
   * Append the given title to the page title
   * @param {String} title - Title to append to the page
   * @param {String} [suffix=Datashare] - Suffix to the title
   */
  setPageTitle(title = null, suffix = 'Datashare') {
    if (document && document.title) {
      document.title = title ? `${title} - ${suffix}` : suffix
    }
  }
  /**
   * Get a promise that is resolved when the application is ready
   * @fullfil {Object} The actual application core instance.
   * @type {Promise<Object>}
   */
  get ready() {
    if (!this._ready) {
      this.defer()
    }
    return this._ready
  }
  /**
   * The application core instance. Deprecated in favor or the `core` property.
   * @type {Core}
   * @deprecated
   */
  get app() {
    return this
  }
  /**
   * The application core instance
   * @type {Core}
   */
  get core() {
    return this
  }
  /**
   * The Vue class to instantiate the application with
   * @type {Vue}
   */
  get localVue() {
    return this.LocalVue
  }
  /**
   * The Vuex instance
   * @type {Vuex.Store}
   */
  get store() {
    return this._store
  }
  /**
   * The Auth module instance
   * @type {Auth}
   */
  get auth() {
    return this._auth
  }
  /**
   * The configuration object provided by Murmur
   * @type {Object}
   */
  get config() {
    return Murmur.config
  }
  /**
   * The Datashare api interface
   * @type {Api}
   */
  get api() {
    return this._api
  }
  /**
   * Get current Datashare mode
   * @type {String}
   */
  get mode() {
    return getMode(this.config.get('mode'))
  }
  /**
   * instantiate a Core class (useful for chaining usage or mapping)
   * @param {...Mixed} options - Options to pass to the Core constructor
   * @returns {Core}
   */
  static init(...options) {
    return new Core(...options)
  }
}

// Force usage of Core.init instead of constructor
const coreInit = Object.freeze({
  isInstanceOfCore: (object) => object instanceof Core,
  init: Core.init
})
export default coreInit