import * as _ from 'lodash'
import BaseProperty from '../adapters/base-property'
import PropertyDecorator from './property-decorator'
import ActionDecorator from './action-decorator'
import ViewHelpers from '../utils/view-helpers'
import ConfigurationError from '../utils/configuration-error'
import BaseResource from '../adapters/base-resource'
import AdminBro from '../../admin-bro'
import * as ACTIONS from '../actions/index'
import { ResourceOptions } from './resource-options.interface'
import Action, { ActionResponse } from '../actions/action.interface'
import { CurrentAdmin } from '../../current-admin.interface'
import ResourceJSON from './resource-json.interface'
import { PropertyPlace } from './property-json.interface'
import BaseRecord from '../adapters/base-record'
/**
* Default maximum number of items which should be present in a list.
*
* @type {Number}
* @private
*/
export const DEFAULT_MAX_COLUMNS_IN_LIST = 8
/**
* Base decorator class which decorates the Resource.
*
* @category Decorators
*/
class ResourceDecorator {
public properties: {[key: string]: PropertyDecorator}
public options: ResourceOptions
public actions: {[key: string]: ActionDecorator}
private _resource: BaseResource
private _admin: AdminBro
private h: ViewHelpers
/**
* @param {object} options
* @param {BaseResource} options.resource resource which is decorated
* @param {AdminBro} options.admin current instance of AdminBro
* @param {ResourceOptions} [options.options]
*/
constructor({ resource, admin, options = {} }: {
resource: BaseResource;
admin: AdminBro;
options: ResourceOptions;
}) {
this.getPropertyByKey = this.getPropertyByKey.bind(this)
this._resource = resource
this._admin = admin
this.h = new ViewHelpers({ options: admin.options })
/**
* Options passed along with a given resource
* @type {ResourceOptions}
*/
this.options = options
this.options.properties = this.options.properties || {}
/**
* List of all decorated properties
* @type {Array<PropertyDecorator>}
*/
this.properties = this.decorateProperties()
/**
* Actions for a resource
* @type {Object<String, ActionDecorator>}
*/
this.actions = this.decorateActions()
}
/**
* Used to create an {@link ActionDecorator} based on both
* {@link AdminBro.ACTIONS default actions} and actions specified by the user
* via {@link AdminBroOptions}
*
* @returns {Record<string, ActionDecorator>}
*/
decorateActions(): {[key: string]: ActionDecorator} {
// in the end we merge actions defined by the user with the default actions.
// since _.merge is a deep merge it also overrides defaults with the parameters
// specified by the user.
const actions = _.merge({}, ACTIONS, this.options.actions || {})
const returnActions = {}
// setting default values for actions
Object.keys(actions).forEach((key: string) => {
const action: Action<ActionResponse> = {
name: actions[key].name || key,
label: actions[key].label || key,
actionType: actions[key].actionType || ['resource'],
handler: actions[key].handler || (async (): Promise<void> => {
console.log('You have to define handler function')
}),
...actions[key],
}
// action.name = action.name ? action.name : key
// action.label = action.label || key
// actions[key].handler = actions[key].handler
returnActions[key] = new ActionDecorator({
action,
admin: this._admin,
resource: this._resource,
})
})
return returnActions
}
/**
* Initializes PropertyDecorator for all properties within a resource. When
* user passes new property in the options - it will be created as well.
*
* @returns {Object<string,PropertyDecorator>}
* @private
*/
decorateProperties(): {[key: string]: PropertyDecorator} {
const resourceProperties = this._resource.properties()
// decorate all existing properties
const properties = resourceProperties.reduce((memo, property) => {
const decorator = new PropertyDecorator({
property,
admin: this._admin,
options: this.options.properties && this.options.properties[property.name()],
resource: this,
})
return { ...memo, [property.name()]: decorator }
}, {})
if (this.options.properties) {
// decorate all properties user gave in options but they don't exist in the resource
Object.keys(this.options.properties).forEach((key) => {
if (!properties[key] && !key.match(/\./)) {
const property = new BaseProperty({ path: key, isSortable: false })
properties[key] = new PropertyDecorator({
property,
admin: this._admin,
options: this.options.properties && this.options.properties[key],
resource: this,
})
}
})
}
return properties
}
/**
* Returns the name for the resource.
* @return {string} resource name
*/
getResourceName(): string {
return this.options.name || this._resource.name()
}
/**
* Returns resource parent along with the icon. By default it is a
* database type with its icon
* @return {Record<string,string>} returns { name, icon }
*/
getParent(): {name: string; icon: string} {
const parent = (
this.options.parent || this._resource.databaseName()
) as {name: string; icon: string}
const name = (parent.name || parent) as string
const icon = parent.icon ? parent.icon : `icon-${this._resource.databaseType() || 'database'}`
return { name, icon }
}
/**
* Returns propertyDecorator by giving property path
*
* @param {String} propertyPath property path
*
* @return {PropertyDecorator}
* @throws {ConfigurationError} when there is no property for given key
*/
getPropertyByKey(propertyPath: string): PropertyDecorator {
const property = this.properties[propertyPath]
if (!property) {
throw new ConfigurationError(
`there is no property by the name of '${propertyPath}' in resource ${this.getResourceName()}`,
'tutorial-04-customizing-resources.html',
)
}
return property
}
/**
* Returns list of all properties which will be visible in given place (where)
*
* @param {Object} options
* @param {String} options.where one of: 'list', 'show', 'edit', 'filter'
* @param {String} [options.max] maximum number of properties returned where there are
* no overrides in the options
*
* @return {Array<PropertyDecorator>}
*/
getProperties({ where, max = 0 }: {
where: PropertyPlace;
max?: number;
}): Array<PropertyDecorator> {
const whereProperties = `${where}Properties` // like listProperties, viewProperties etc
if (this.options[whereProperties] && this.options[whereProperties].length) {
return this.options[whereProperties].map(this.getPropertyByKey)
}
const properties = Object.keys(this.properties)
.filter(key => this.properties[key].isVisible(where))
.sort((key1, key2) => (
this.properties[key1].position()
> this.properties[key2].position() ? 1 : -1))
.map(key => this.properties[key])
if (max) {
return properties.slice(0, max)
}
return properties
}
getListProperties(): Array<PropertyDecorator> {
return this.getProperties({ where: PropertyPlace.list, max: DEFAULT_MAX_COLUMNS_IN_LIST })
}
/**
* List of all actions which should be invoked for entire resource and not
* for a particular record
*
* @param {CurrentAdmin} currentAdmin currently logged in admin user
* @return {Array<ActionDecorator>} Actions assigned to resources
*/
resourceActions(currentAdmin?: CurrentAdmin): Array<ActionDecorator> {
return Object.values(this.actions)
.filter(action => (
action.isResourceType()
&& action.isVisible(currentAdmin)
&& action.isAccessible(currentAdmin)
))
}
/**
* List of all actions which should be invoked for entire resource and not
* for a particular record
*
* @param {CurrentAdmin} currentAdmin currently logged in admin user
* @return {Array<ActionDecorator>} Actions assigned to resources
*/
bulkActions(record: BaseRecord, currentAdmin?: CurrentAdmin): Array<ActionDecorator> {
return Object.values(this.actions)
.filter(action => (
action.isBulkType()
&& action.isVisible(currentAdmin, record)
&& action.isAccessible(currentAdmin, record)
))
}
/**
* List of all actions which should be invoked for given record and not
* for an entire resource
*
* @param {CurrentAdmin} [currentAdmin] currently logged in admin user
* @return {Array<ActionDecorator>} Actions assigned to each record
*/
recordActions(record: BaseRecord, currentAdmin?: CurrentAdmin): Array<ActionDecorator> {
return Object.values(this.actions)
.filter(action => (
action.isRecordType()
&& action.isVisible(currentAdmin, record)
&& action.isAccessible(currentAdmin, record)
))
}
/**
* Returns PropertyDecorator of a property which should be treated as a title property.
*
* @return {PropertyDecorator} PropertyDecorator of title property
*/
titleProperty(): PropertyDecorator {
const properties = Object.values(this.properties)
const titleProperty = properties.find(p => p.isTitle())
return titleProperty || properties[0]
}
/**
* Returns title for given record.
*
* For example: If given record has `name` property and this property has `isTitle` flag set in
* options or by the Adapter - value for this property will be shown
*
* @param {BaseRecord} record
*
* @return {String} title of given record
*/
titleOf(record: BaseRecord): string {
return record.param(this.titleProperty().name())
}
/**
* Returns JSON representation of a resource
*
* @param {CurrentAdmin} currentAdmin
* @return {ResourceJSON}
*/
toJSON(currentAdmin?: CurrentAdmin): ResourceJSON {
return {
id: this._resource.id(),
name: this.getResourceName(),
parent: this.getParent(),
href: this.h.resourceActionUrl({ resourceId: this._resource.id(), actionName: 'list' }),
titleProperty: this.titleProperty().toJSON(),
resourceActions: this.resourceActions(currentAdmin).map(ra => ra.toJSON()),
listProperties: this.getProperties({
where: PropertyPlace.list, max: DEFAULT_MAX_COLUMNS_IN_LIST,
}).map(property => property.toJSON()),
editProperties: this.getProperties({
where: PropertyPlace.edit,
}).map(property => property.toJSON()),
showProperties: this.getProperties({
where: PropertyPlace.show,
}).map(property => property.toJSON()),
filterProperties: this.getProperties({
where: PropertyPlace.filter,
}).map(property => property.toJSON()),
}
}
}
export default ResourceDecorator
Source