ember-frost-bunsen
Version:
Create UI's from JSON configurations.
325 lines (275 loc) • 9.77 kB
JavaScript
import Ember from 'ember'
const {typeOf} = Ember // eslint-disable-line
import config from 'ember-get-config'
import _ from 'lodash'
const assign = Ember.assign || Object.assign || Ember.merge // eslint-disable-line
/**
* @typedef {Object} Facet
* @property {String} label - label to use for facet
* @property {String} model - model property to generate facet for
* @property {BunsenRenderer} [renderer] - renderer to use for facet
*/
export const builtInRenderers = {
boolean: 'frost-bunsen-input-boolean',
'button-group': 'frost-bunsen-input-button-group',
'checkbox-array': 'frost-bunsen-input-checkbox-array',
date: 'frost-bunsen-input-date',
geolocation: 'frost-bunsen-input-geolocation',
hidden: 'frost-bunsen-input-hidden',
image: 'frost-bunsen-input-image',
integer: 'frost-bunsen-input-number',
json: 'frost-bunsen-input-json',
link: 'frost-bunsen-input-link',
'multi-select': 'frost-bunsen-input-multi-select',
number: 'frost-bunsen-input-number',
password: 'frost-bunsen-input-password',
'property-chooser': 'frost-bunsen-input-property-chooser',
select: 'frost-bunsen-input-select',
static: 'frost-bunsen-input-static',
string: 'frost-bunsen-input-text',
textarea: 'frost-bunsen-input-textarea',
url: 'frost-bunsen-input-url'
}
/* eslint-disable complexity */
export function deemberify (emberObject) {
if (emberObject === null || emberObject === undefined) {
return emberObject
}
if (typeOf(emberObject.serialize) === 'function') {
return emberObject.serialize({includeId: true})
}
if (emberObject.content) {
return emberObject.content.map((contentObject) => {
return JSON.parse(JSON.stringify(contentObject._data))
})
}
if (emberObject._prevContent) {
return emberObject._prevContent.map((contentObject) => {
return JSON.parse(JSON.stringify(contentObject._data))
})
}
return JSON.parse(JSON.stringify(emberObject))
}
/* eslint-enable complexity */
/**
* Generate label from bunsen model
* @param {String} model - bunsen model/property path
* @returns {String} model converted to label
*/
export function generateLabelFromModel (model) {
const property = model.split('.').pop()
const dasherizedName = Ember.String.dasherize(property).replace('-', ' ')
return Ember.String.capitalize(dasherizedName)
}
/**
* Generate cell for a facet
* @param {Facet} facet - facet to generate cell for
* @returns {BunsenCell} bunsen cell for facet
*/
export function generateFacetCell (facet) {
const cell = {
model: facet.model
}
if (facet.renderer) {
cell.renderer = facet.renderer
}
const renderersToHideClearButtonFor = [
'multi-select'
]
const renderersToShowCollapseHandleFor = [
'checkbox-array',
'geolocation',
'json',
'textarea'
]
const clearable = Boolean(
!facet.renderer ||
renderersToHideClearButtonFor.indexOf(facet.renderer.name) === -1
)
const collapsible = Boolean(
facet.renderer &&
renderersToShowCollapseHandleFor.indexOf(facet.renderer.name) !== -1
)
return {
children: [cell],
clearable,
collapsible,
label: facet.label || generateLabelFromModel(facet.model)
}
}
/**
* Generate bunsen view for facets
* @param {Facet[]} facets - facets to generate view for
* @returns {BunsenView} bunsen view for facets
*/
export function generateFacetView (facets) {
return {
cells: [
{
children: facets.map(generateFacetCell)
}
],
type: 'form',
version: '2.0'
}
}
/**
* Get merged definition for current cell
* @param {BunsenCell} cellConfig - current cell
* @param {Object<String, BunsenCell>} cellDefinitions - list of cell definitions
* @returns {BunsenCell} current merged cell definition
*/
export function getMergedConfig (cellConfig, cellDefinitions) {
if (!cellConfig.extends) {
return _.cloneDeep(cellConfig)
}
const superCell = getMergedConfig(cellDefinitions[cellConfig.extends], cellDefinitions)
const mergedConfig = assign(superCell, cellConfig)
delete mergedConfig.extends
return mergedConfig
}
/**
* Merges the cellConfig recursively
* @param {BunsenCell} cellConfig - top-level cell
* @param {Object<String, BunsenCell>} cellDefinitions - list of cell definitions
* @returns {BunsenCell} merged cell definition
*/
export function getMergedConfigRecursive (cellConfig, cellDefinitions) {
const mergedConfig = getMergedConfig(cellConfig, cellDefinitions)
// recursive object case
if (mergedConfig.children) {
const mergedChildConfigs = []
mergedConfig.children.forEach((childConfig) => {
mergedChildConfigs.push(getMergedConfigRecursive(childConfig, cellDefinitions))
})
mergedConfig.children = mergedChildConfigs
}
// recursive array case
if (mergedConfig.arrayOptions && mergedConfig.arrayOptions.itemCell) {
mergedConfig.arrayOptions.itemCell = getMergedConfigRecursive(mergedConfig.arrayOptions.itemCell, cellDefinitions)
}
return mergedConfig
}
/**
* Determine if model is registered with Ember Data
* @param {String} modelName - name of model to check Ember Data registry for
* @returns {Boolean} whether or not model is registered with Ember Data
*/
export function isRegisteredEmberDataModel (modelName) {
return Object.keys(require._eak_seen)
.filter((module) => {
return (
module.indexOf(`${config.modulePrefix}/models/`) === 0 ||
module.indexOf(`${config.podModulePrefix}/models/`) === 0
)
})
.map((module) => {
return module
.replace(`${config.modulePrefix}/models/`, '')
.replace(`${config.podModulePrefix}/models/`, '')
})
.indexOf(modelName) !== -1
}
export function getRendererComponentName (rendererName) {
return builtInRenderers[rendererName] || rendererName
}
/**
* Determine if an input is required to submit form
* @param {String} path - path to property in bunsen model
* @param {BunsemModel} bunsenModel - bunsen model
* @param {Object} value - form value
* @param {Boolean} parentRequired - whether or not parent model is required
* @returns {Boolean} whether or not last path is required
*/
export function isChildRequiredToSubmitForm (path, bunsenModel, value, parentRequired) {
const isChildMissing = !_.get(value, path)
let isParentPresent, lastSegment
if (path.indexOf('.') === -1) {
isParentPresent = Boolean(value)
lastSegment = path
} else {
const segments = path.split('.')
lastSegment = segments.pop()
const relativePath = segments.join('.')
isParentPresent = Boolean(_.get(value, relativePath))
while (segments.length !== 0) {
const segment = segments.splice(0, 1)[0]
parentRequired = Boolean(
parentRequired &&
bunsenModel.required &&
bunsenModel.required.indexOf(segment) !== -1
)
if (/^\d+$/.test(segment)) {
bunsenModel = bunsenModel.items
} else {
bunsenModel = bunsenModel.properties[segment]
}
}
}
const childIsRequiredByParent = bunsenModel.required && bunsenModel.required.indexOf(lastSegment) !== -1
return (
childIsRequiredByParent &&
(isParentPresent || parentRequired) &&
isChildMissing
)
}
/**
* Determine whether or not cell contains required inputs
* @param {BunsenCell} cell - bunsen view cell
* @param {Object<String, BunsenCell>} cellDefinitions - list of cell definitions
* @param {BunsenModel} bunsenModel - bunsen model
* @param {Object} value - form value
* @param {Boolean} parentRequired - whether or not parent model is required
* @returns {Boolean} whether or not cell contains required inputs
*/
export function isRequired (cell, cellDefinitions, bunsenModel, value, parentRequired = true) {
cell = getMergedConfig(cell, cellDefinitions)
// If the view cell doesn't contain children we can just determine if the model property is required
if (!cell.children) {
return isChildRequiredToSubmitForm(cell.model, bunsenModel, value, parentRequired)
}
// If the cell has a model defined, that model is applied to all children cells and thus we need to get
// the sub-model of the current bunsenModel that represents this scoped/nested model
if (cell.model) {
const modelSegments = cell.model.split('.')
while (modelSegments.length !== 0) {
const segment = modelSegments.splice(0, 1)[0]
parentRequired = Boolean(
parentRequired &&
bunsenModel.required &&
bunsenModel.required.indexOf(segment) !== -1
)
if (/^\d+$/.test(segment)) {
bunsenModel = bunsenModel.items
} else {
bunsenModel = bunsenModel.properties[segment]
}
}
value = _.get(value, cell.model)
}
// If any child view cell is required then the parent cell should be labeled as required in the UI
return cell.children
.some((child) => isRequired(child, cellDefinitions, bunsenModel, value, parentRequired))
}
export function validateRenderer (owner, rendererName) {
return rendererName in builtInRenderers || owner.hasRegistration(`component:${rendererName}`)
}
/**
* Get an error message from an error Object
* @param {Object} error - may be an Error, an API response, or anything else (maybe?)
* @returns {String} the message for the error
*/
export function getErrorMessage (error) {
let message = 'Unable to parse error object'
// most common case will be a JSON-API error response from ember-data
message = _.get(error, 'responseJSON.errors[0].detail')
if (!message) {
// next common is maybe an Error Object
message = _.get(error, 'message')
}
if (!message && error.toString) {
// next option is anything else with a .toString() method
message = error.toString()
}
return message
}