Source

adminjs/src/backend/decorators/resource/resource-decorator.ts

import { DecoratedActions } from './utils/decorate-actions'

import { BaseResource, BaseRecord } from '../../adapters'
import { PropertyDecorator, ActionDecorator } from '..'
import ViewHelpers from '../../utils/view-helpers/view-helpers'
import AdminJS from '../../../adminjs'

import { ResourceOptions } from './resource-options.interface'
import { CurrentAdmin } from '../../../current-admin.interface'
import { ResourceJSON, PropertyPlace } from '../../../frontend/interfaces'
import {
  decorateActions,
  decorateProperties,
  getNavigation,
  flatSubProperties,
  DecoratedProperties,
  getPropertyByKey,
} from './utils'


/**
 * 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 {
  /**
   * Map of all root level properties. By root properties we mean property which is not nested
   * under other mixed property.
   *
   * Examples from PropertyOptions:
   * {
   *   rootProperty: { type: mixed }, // root property
   *
   *    // nested property - this should go be the subProperty of rootProperty
   *   'rootProperty.nested': { type: 'string' }
   *
   *   // also root property because there is no another property of type mixed
   *   'another.property': { type: 'string' },
   * }
   *
   * for a the reference {@see decorateProperties}
   */
  public properties: DecoratedProperties

  public options: ResourceOptions

  public actions: DecoratedActions

  private _resource: BaseResource

  private _admin: AdminJS

  private h: ViewHelpers

  /**
   * @param  {object}       options
   * @param  {BaseResource} options.resource  resource which is decorated
   * @param  {AdminJS}     options.admin  current instance of AdminJS
   * @param  {ResourceOptions} [options.options]
   */
  constructor({ resource, admin, options = {} }: {
    resource: BaseResource;
    admin: AdminJS;
    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 root properties
     * @type {Array<PropertyDecorator>}
     */
    this.properties = decorateProperties(resource, admin, this)

    /**
     * Actions for a resource
     * @type {Object<String, ActionDecorator>}
     */
    this.actions = decorateActions(resource, admin, this)
  }

  /**
   * Returns the name for the resource.
   * @return {string} resource name
   */
  getResourceName(): string {
    return this._admin.translateLabel(this.id(), this.id())
  }

  /**
   * Returns the id for the resource.
   * @return {string} resource id
   */
  id(): string {
    return this.options.id || this._resource.id()
  }

  /**
   * Returns resource parent along with the icon. By default it is a
   * database type with its icon
   * @return {Parent}   ResourceJSON['parent']}
   */
  getNavigation(): ResourceJSON['navigation'] {
    return getNavigation(this.options, this._resource)
  }

  /**
   * Returns propertyDecorator by giving property path
   *
   * @param   {String}  propertyPath  property path
   *
   * @return  {PropertyDecorator}
   */
  getPropertyByKey(propertyPath: string): PropertyDecorator | null {
    return getPropertyByKey(propertyPath, this.properties)
  }

  /**
   * 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 (where && this.options[whereProperties] && this.options[whereProperties].length) {
      return this.options[whereProperties]
        .map((propertyName) => {
          const property = this.getPropertyByKey(propertyName)
          if (!property) {
            // eslint-disable-next-line no-console
            console.error([
              `[AdminJS]: There is no property of the name: "${propertyName}".`,
              `Check out the "${where}Properties" in the`,
              `resource: "${this._resource.id()}"`].join(' '))
          }
          return property
        }).filter(property => property)
    }

    const properties = Object.keys(this.properties)
      .filter(key => !where || 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
  }

  /**
   * Returns all the properties with corresponding subProperties in one object.
   */
  getFlattenProperties(): Record<string, PropertyDecorator> {
    return Object.keys(this.properties).reduce((memo, propertyName) => {
      const property = this.properties[propertyName]

      const subProperties = flatSubProperties(property)
      return Object.assign(memo, { [propertyName]: property }, subProperties)
    }, {})
  }

  getListProperties(): Array<PropertyDecorator> {
    return this.getProperties({ where: '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.get(this.titleProperty().name()) as string
  }

  getHref(currentAdmin?: CurrentAdmin): string | null {
    const { href } = this.options
    if (href) {
      if (typeof href === 'function') {
        return href({
          resource: this._resource,
          currentAdmin,
          h: this.h,
        })
      }
      return href
    }
    if (this.resourceActions(currentAdmin).find(action => action.name === 'list')) {
      return this.h.resourceUrl({ resourceId: this.id() })
    }
    return null
  }

  /**
   * Returns JSON representation of a resource
   *
   * @param {CurrentAdmin} currentAdmin
   * @return  {ResourceJSON}
   */
  toJSON(currentAdmin?: CurrentAdmin): ResourceJSON {
    const flattenProperties = this.getFlattenProperties()
    const flattenPropertiesJSON = Object.keys(flattenProperties).reduce((memo, key) => ({
      ...memo,
      [key]: flattenProperties[key].toJSON(),
    }), {})

    return {
      id: this.id(),
      name: this.getResourceName(),
      navigation: this.getNavigation(),
      href: this.getHref(currentAdmin),
      titleProperty: this.titleProperty().toJSON(),
      resourceActions: this.resourceActions(currentAdmin).map(ra => ra.toJSON(currentAdmin)),
      actions: Object.values(this.actions).map(action => action.toJSON(currentAdmin)),
      properties: flattenPropertiesJSON,
      listProperties: this.getProperties({
        where: 'list', max: DEFAULT_MAX_COLUMNS_IN_LIST,
      }).map(property => property.toJSON('list')),
      editProperties: this.getProperties({
        where: 'edit',
      }).map(property => property.toJSON('edit')),
      showProperties: this.getProperties({
        where: 'show',
      }).map(property => property.toJSON('show')),
      filterProperties: this.getProperties({
        where: 'filter',
      }).map(property => property.toJSON('filter')),
    }
  }
}

export default ResourceDecorator