jsreport-studio
Version:
jsreport templates editor and designer
768 lines (671 loc) • 20.2 kB
JavaScript
import PropTypes from 'prop-types'
import React from 'react'
import ReactDom from 'react-dom'
import { NativeTypes } from 'react-dnd-html5-backend'
import ReactList from 'react-list'
import superagent from 'superagent'
import shortid from 'shortid'
import fileSaver from 'filesaver.js-npm'
import _merge from 'lodash/merge'
import api, { methods } from './helpers/api.js'
import { getCurrentTheme, setCurrentTheme } from './helpers/theme.js'
import SplitPane from './components/common/SplitPane/SplitPane.js'
import Popover from './components/common/Popover/index.js'
import MultiSelect from './components/common/MultiSelect/index.js'
import EntityRefSelect from './components/common/EntityRefSelect/index.js'
import TextEditor from './components/Editor/TextEditor.js'
import EntityTree from './components/EntityTree/EntityTree.js'
import EntityTreeButton from './components/EntityTree/EntityTreeButton.js'
import Preview from './components/Preview/Preview.js'
import NewEntityModal from './components/Modals/NewEntityModal.js'
import * as editor from './redux/editor'
import * as entities from './redux/entities'
import * as progress from './redux/progress'
import * as settings from './redux/settings'
import * as configuration from './lib/configuration.js'
import resolveUrl from './helpers/resolveUrl.js'
import babelRuntime from './lib/babelRuntime.js'
import customPreview from './helpers/customPreview.js'
import bluebird from 'bluebird'
import io from 'socket.io-client'
/**
* Main facade and API for extensions. Exposed as global variable Studio. It can be also imported from jsreport-studio
* when using extensions default webpack configuration
* @class
* @public
*/
class Studio {
/** event listeners **/
/**
* Array of async functions invoked in sequence during initialization
* @returns {Function[]}
*/
get initializeListeners () {
return configuration.initializeListeners
}
/**
* Array of async functions invoked in sequence after the app has been rendered
* @returns {Function[]}
*/
get readyListeners () {
return configuration.readyListeners
}
/**
* Array of async functions invoked in sequence when preview process starts.
* @returns {Function[]}
*/
get previewListeners () {
return configuration.previewListeners
}
get textEditorInitializeListeners () {
return configuration.textEditorInitializeListeners
}
get textEditorCreatedListeners () {
return configuration.textEditorCreatedListeners
}
/** /event listeners **/
/** initial configuration **/
/**
* Add new entity set, which will be automatically loaded through OData and displayed in the entity tree
* @example Studio.addEntitySet({ name: 'data', visibleName: 'sample data' })
* @param {Object} entitySet
*/
addEntitySet (entitySet) {
entitySet.nameAttribute = entitySet.nameAttribute || 'name'
entitySet.referenceAttributes = [...new Set([...(entitySet.referenceAttributes || []), entitySet.nameAttribute, 'shortid'])]
configuration.entitySets[entitySet.name] = entitySet
}
/**
* Add React component which will be displayed in toolbar
*
* @param {ReactComponent|Function} toolbarComponent
* @param {String} position left, right, settings or settingsBottom
*/
addToolbarComponent (toolbarComponent, position = 'left') {
configuration.toolbarComponents[position].push(toolbarComponent)
}
/**
* Add React component which will be displayed as a wrapper/container for entity tree
*
* @param {ReactComponent|Function} entityTreeWrapperComponent
*/
addEntityTreeWrapperComponent (entityTreeWrapperComponent) {
configuration.entityTreeWrapperComponents.push(entityTreeWrapperComponent)
}
/**
* Add React component which will be displayed in toolbar of entity tree
*
* @param {ReactComponent|Function} entityTreeToolbarComponent
*/
addEntityTreeToolbarComponent (entityTreeToolbarComponent, position = 'single') {
configuration.entityTreeToolbarComponents[position].push(entityTreeToolbarComponent)
}
/**
* Add React component which will be displayed when rendering an item of entity tree
*
* @param {ReactComponent|Function} entityTreeItemComponent
* @param {String} position right, groupRight or container
*/
addEntityTreeItemComponent (entityTreeItemComponent, position = 'right') {
configuration.entityTreeItemComponents[position].push(entityTreeItemComponent)
}
/**
* Add a fn to resolve items for the conext menu at Entity Tree
* @param {Function} fn
*/
addEntityTreeContextMenuItemsResolver (fn) {
configuration.entityTreeContextMenuItemsResolvers.push(fn)
}
/**
* Add React component which will be used as tab title
*
* @param {String} key used in openTab
* @param {ReactComponent|Function} component
*/
addTabTitleComponent (key, component) {
configuration.tabTitleComponents[key] = component
}
/**
* Add component used in tab as content editor
*
* @param {String} key - key used in openTab({ editorComponentKey: ... , use entity set name if the editor should represent the main entity editor
* @param {ReactComponent|Function} component
* @param {Function} reformat - function handling reformatting code
*/
addEditorComponent (key, component, reformat) {
configuration.editorComponents[key] = component
configuration.editorComponents[key].reformat = reformat
}
/**
* Add component used in the left Properties secion
*
* @param {Function|String} string or title function used to render the section title
* @param {ReactComponent|Function} component
* @param {Function} shouldDisplay
*/
addPropertiesComponent (title, component, shouldDisplay) {
configuration.propertiesComponents.push({
title: title,
component: component,
shouldDisplay: shouldDisplay
})
}
/**
* Array of functions used to resolve ace editor mode for template content. This is used by custom templating engines
* to add highlighting support for jade,ejs...
*
* @returns {Function[]}
*/
get templateEditorModeResolvers () {
return configuration.templateEditorModeResolvers
}
/**
* Array of functions used to resolve entity icon in entity tree, function accepts entity and returns string like fa-cog
*
* @returns {Function[]}
*/
get entityTreeIconResolvers () {
return configuration.entityTreeIconResolvers
}
/**
* Array of functions used to resolve filtering in entity tree,
* function accepts entity, entitySets and filter info, should return boolean to determine if
* item should be skipped or not
*
* @returns {Function[]}
*/
get entityTreeFilterItemResolvers () {
return configuration.entityTreeFilterItemResolvers
}
/**
* Array of functions used to resolve drop into entity tree
*
* @returns {Function[]}
*/
get entityTreeDropResolvers () {
return configuration.entityTreeDropResolvers
}
/**
* Array of functions used to resolve entity editor component editor, function accepts entity and returns string represent the component editor key
*
* @returns {Function[]}
*/
get entityEditorComponentKeyResolvers () {
return configuration.entityEditorComponentKeyResolvers
}
/**
* Sets the function returning the browser url path
* (defaultCalculatedPath, currentEntity) => String
* @param {Function} fn
*/
set locationResolver (fn) {
configuration.locationResolver = fn
}
/**
* Set the function retunring the visibility flag for particular toolbar button
* ('Save All') => return true
* @param {Function} fn
*/
set toolbarVisibilityResolver (fn) {
configuration.toolbarVisibilityResolver = fn
}
/**
* Override the default entities references loading with custom function
* (entitySet) => Promise([array])
* @param {Function} fn
*/
set referencesLoader (fn) {
configuration.referencesLoader = fn
}
/**
* Optionally you can avoid displaying default startup page
* @param {Boolean} trueOrFalse
*/
set shouldOpenStartupPage (trueOrFalse) {
configuration.shouldOpenStartupPage = trueOrFalse
}
/**
* Override the default entity remove behavior
* (id) => {})
* @param {Function} fn
*/
set removeHandler (fn) {
configuration.removeHandler = fn
}
/**
* Set additional custom header to all api calls
* @param {String} key
* @param {String} value
*/
setRequestHeader (key, value) {
configuration.apiHeaders[key] = value
}
setAboutModal (AboutModalComponent) {
configuration.aboutModal = AboutModalComponent
}
/**
* Merges in the object defining the api which is used in api fialog
* @param {Object} obj
*/
addApiSpec (obj) {
_merge(configuration.apiSpecs, obj)
}
/** /initial configuration **/
/** runtime helpers **/
/**
* Override the right preview pane with additional content
* setPreviewFrameSrc('data:text/html;charset=utf-8,foooooooo')
* @param {String} frameSrc
*/
setPreviewFrameSrc (frameSrc) {
configuration.previewFrameChangeHandler(frameSrc)
}
/**
* Display custom content in the preview pane using http post to the url
* This is usefull when Studio.setPreviewFrameSrc isn't working because the content to set is too big
* and hits the iframe src chars limit.
* @param {String} frameSrc
*/
customPreview (url, request, opts = {}) {
const target = configuration.getPreviewTargetHandler() || 'previewFrame'
configuration.previewConfigurationHandler({ ...opts, src: null }).then(() => {
customPreview(url, request, target)
})
}
/**
* Provides methods get,patch,post,del for accessing jsreport server
*
* @example
* await Studio.api.patch('/odata/tasks', { data: { foo: '1' } })
*
* @returns {*}
*/
get api () {
return this.API
}
/**
* Get registered entity sets, each one is object { visibleName: 'foo', nameAttribute: 'name' }
* @returns {Object[]}
*/
get entitySets () {
return configuration.entitySets
}
/**
* Object[name] with registered extensions and its options
* @returns {Object}
*/
get extensions () {
return configuration.extensions
}
/**
* Opens modal dialog.
*
* @param {ReacrComponent|String}componentOrText
* @param {Object} options passed as props to the react component
*/
openModal (componentOrText, options) {
configuration.modalHandler.open(componentOrText, options || {})
}
openNewModal (entitySet) {
configuration.modalHandler.open(NewEntityModal, { entitySet: entitySet })
}
isModalOpen () {
return configuration.modalHandler.isModalOpen()
}
/**
* Invoke preview process for last active template
*/
preview () {
configuration.previewHandler()
}
/**
* Collapse entity in EntityTree
*/
collapseEntity (entityIdOrShortid, state = true, options = {}) {
configuration.collapseEntityHandler(entityIdOrShortid, state, options)
}
/**
* Collapse left pane
*/
collapseLeftPane (type = true) {
configuration.collapseLeftHandler(type)
}
/**
* Collapse preview pane
*/
collapsePreviewPane (type = true) {
configuration.collapsePreviewHandler(type)
}
/**
* Open and activate new editor tab
*
* @example
* //open entity editor
* Studio.openTab({ _id: 'myentityid' })
* //open custom page
* Studio.openTab({ key: 'StartupPage', editorComponentKey: 'startup', title: 'Statup' })
*
* @param {Object} tab
*/
openTab (tab) {
return this.store.dispatch(editor.actions.openTab(tab))
}
/**
* Loads entity, which reference is already present in the ui state, from the remote API
*
* @param {String} id
* @param {Boolean} force
* @return {Promise}
*/
loadEntity (id, force = false) {
return this.store.dispatch(entities.actions.load(id, force))
}
/**
* Remove the additional entity properties from the state, keep just meta and id
* @param {String} id
*/
unloadEntity (id) {
return this.store.dispatch(entities.actions.unload(id))
}
/**
* Add entity to the state
* @param {Object} entity
*/
addEntity (entity) {
this.store.dispatch(entities.actions.add(entity))
}
/**
* Update entity in the state
* @param {Object} entity
*/
updateEntity (entity) {
this.store.dispatch(entities.actions.update(entity))
}
/**
* Call remote API and persist (insert or update) entity
* @param {String} id
* @return {Promise}
*/
saveEntity (id, opts) {
return this.store.dispatch(entities.actions.save(id, opts))
}
/**
* Adds already existing (persisted) entity into the UI state
* @param entity
*/
addExistingEntity (entity) {
this.store.dispatch(entities.actions.addExisting(entity))
}
/**
* Replace the existing entity in the state
* @param {String} oldId
* @param {Object} entity
*/
replaceEntity (oldId, entity) {
this.store.dispatch(entities.actions.replace(oldId, entity))
}
/**
* Remove entity from the state
* @param {String} id
*/
removeEntity (id) {
this.store.dispatch({
type: entities.ActionTypes.REMOVE,
_id: id
})
}
/**
* Show ui signalization for running background operation
*/
startProgress () {
this.store.dispatch(progress.actions.start())
}
/**
* Hide ui signalization for running background operation
*/
stopProgress () {
this.store.dispatch(progress.actions.stop())
}
/**
* Emits an error that shows the message in a modal
* @param {Error} e
* @param {Boolean} ignoreModal defaults to false
*/
apiFailed (...args) {
return this.store.dispatch(entities.actions.apiFailed(...args))
}
/**
* Synchronize the location with history
*/
updateHistory () {
this.store.dispatch(editor.actions.updateHistory())
}
/**
* Clear the current state and reload internally studio
*/
async reset () {
await this.store.dispatch({ type: 'RESET' })
await this.store.dispatch(editor.actions.updateHistory())
await this.store.dispatch(settings.actions.load())
await bluebird.all(Object.keys(this.entitySets).map((t) => this.store.dispatch(entities.actions.loadReferences(t))))
}
/**
* Get the current theme (it will check localstorage for user preference and fallback to the default theme configured)
* @returns {Object[]}
*/
getCurrentTheme () {
return getCurrentTheme()
}
setCurrentTheme (themeInfo, opts) {
return setCurrentTheme(themeInfo, opts)
}
/**
* Get all settings from state
* @returns {Object[]}
*/
getSettings () {
return settings.selectors.getAll(this.store.getState())
}
/**
* Save one setting in state and persist it on the server
* @param {String} key
* @param {Object} value
*/
setSetting (key, value) {
return this.store.dispatch(settings.actions.update(key, value))
}
/**
* Get one setting value from the state
* @param {String} key
* @param {Boolean} shouldThrow
*/
getSettingValueByKey (key, shouldThrow = true) {
return settings.selectors.getValueByKey(this.store.getState(), key, shouldThrow)
}
/**
* Searches for the entity in the UI state based on specified _id
* @param {String} _id
* @param {Boolean} shouldThrow
* @returns {Object|null}
*/
getEntityById (_id, shouldThrow = true) {
return entities.selectors.getById(this.store.getState(), _id, shouldThrow)
}
/**
* Searches for the entity in the UI state based on specified shortid
* @param {String} shortid
* @param {Boolean} shouldThrow
* @returns {Object|null}
*/
getEntityByShortid (shortid, shouldThrow = true) {
return entities.selectors.getByShortid(this.store.getState(), shortid, shouldThrow)
}
/**
* Returns the currently selected entity or null
* @returns {Object}
*/
getActiveEntity () {
return editor.selectors.getActiveEntity(this.store.getState())
}
/**
* Returns last active entity
* @returns {Object|null}
*/
getLastActiveTemplate () {
return editor.selectors.getLastActiveTemplate(this.store.getState())
}
/**
* Get all entities including meta attributes in array
* @returns {Object[]}
*/
getAllEntities () {
return entities.selectors.getAll(this.store.getState())
}
/**
* Get references to entities
* @returns {Object[]}
*/
getReferences () {
return entities.selectors.getReferences(this.store.getState())
}
/**
* Get the path in absolute form like /api/images and make it working also for jsreport running on subpath like myserver.com/reporting/api/images
* @param {String} path
* @returns {String}
*/
resolveUrl (path) {
return resolveUrl(path)
}
/**
* Assemble entity absolute path
* @param {*} entity
* @returns {String}
*/
resolveEntityPath (entity) {
return entities.selectors.resolveEntityPath(this.store.getState(), entity)
}
relativizeUrl (path) {
console.trace('relativizeUrl is deprecated, use resolveUrl')
return resolveUrl(path)
}
/**
* absolute root url to the server, like http://localhost/reporting
* @returns {string}
*/
get rootUrl () {
let url
if (window.location.href.indexOf('/studio') !== -1) {
url = window.location.href.substring(0, window.location.href.indexOf('/studio'))
} else {
url = window.location.href
}
url = url.slice(-1) === '/' ? url.slice(0, -1) : url
return url
}
/** /runtime helpers **/
/** react components **/
/**
* Ace editor React wrapper
*
* @example
* export default class DataEditor extends TextEditor { ... }
*
* @returns {TextEditor}
*/
get TextEditor () {
return TextEditor
}
/**
* Component used to split content with sliders
*
* @returns {SplitPane}
*/
get SplitPane () {
return SplitPane
}
/**
* Component used to show content in a popover
*
* @returns {Popover}
*/
get Popover () {
return Popover
}
/**
* Component used to visualise entities
*
* @returns {EntityTree}
*/
get EntityTree () {
return EntityTree
}
/**
* Component used to add actions in EntityTree toolbar
* @returns {EntityTreeButton}
*/
get EntityTreeButton () {
return EntityTreeButton
}
/**
* Component used for multi-select options
* @returns {MultiSelect}
*/
get MultiSelect () {
return MultiSelect
}
/**
* Component used to select entity refs
* @returns {EntityRefSelect}
*/
get EntityRefSelect () {
return EntityRefSelect
}
get Preview () {
return Preview
}
get dragAndDropNativeTypes () {
return NativeTypes
}
constructor (store) {
this.editor = editor
this.store = store
this.entities = entities
this.settings = settings
this.references = {}
// extensions can add routes, not yet prototyped
this.routes = []
this.API = {}
methods.forEach((m) => {
this.API[m] = (...args) => {
this.startProgress()
return api[m](...args).then((v) => {
this.stopProgress()
return v
}).catch((e) => {
this.stopProgress()
this.store.dispatch(this.entities.actions.apiFailed(e, args[2] === true))
throw e
})
}
})
// webpack replaces all the babel runtime references in extensions with externals taking runtime from this field
// this basically removes the duplicated babel runtime code from extensions and decrease its sizes
this.runtime = babelRuntime
// the same case as for babel runtime, we expose the following libraries and replace their references in extensions
// using webpack externals
this.libraries = {
react: React,
'react-dom': ReactDom,
'prop-types': PropTypes,
'react-list': ReactList,
superagent: superagent,
shortid: shortid,
bluebird: bluebird,
'filesaver.js-npm': fileSaver,
'socket.io-client': io
}
}
}
let studio
export const createStudio = (store) => (studio = new Studio(store))
export default studio