Source

admin-bro/src/frontend/components/property-type/index.tsx

import React, { ReactNode } from 'react'
import { ReactComponentLike } from 'prop-types'
import { Box } from '@admin-bro/design-system'

import ErrorBoundary from '../app/error-boundary'

import * as ArrayType from './array'
import * as MixedType from './mixed'

import * as defaultType from './default-type'
import * as boolean from './boolean'
import * as datetime from './datetime'
import * as richtext from './richtext'
import * as reference from './reference'
import * as textarea from './textarea'
import * as password from './password'
import { BasePropertyProps } from './base-property-props'
import { PropertyType } from '../../../backend/adapters/base-property'

let globalAny: any = {}

try {
  globalAny = window
} catch (error) {
  if (error.message !== 'window is not defined') {
    throw error
  }
}

const types: Record<PropertyType, any> = {
  textarea,
  boolean,
  datetime,
  reference,
  password,
  date: datetime,
  richtext,
  string: defaultType,
  number: defaultType,
  float: defaultType,
  mixed: null,
}

/**
 * Component which renders properties in all the places in the AdminBro UI. By all the
 * places I mean:
 * - **list**: on the List,
 * - **edit**: on default actions where user can modify the record like: {@link EditAction},
 * and {@link NewAction},
 * - **show**: on the default {@link ShowAction} where user can see the details of a record,
 * - **filter**: and finally on the sidebar filter,
 *
 * Based on the type of given property and where the property is rendered **BasePropertyComponent**
 * picks Component to use. That is how **date** fields are rendered as **datepicker**
 * or **boolean** values as **checkbox**'es.
 *
 * ### Overriding default render logic
 *
 * By default BasePropertyComponent will render corresponding
 * component: input for string, DatePicker for dates etc.
 * But you can override this by passing a custom component to {@link PropertyOptions}.
 *
 * Take a look at the following example:
 *
 * ```
 * const AdminBro = require('admin-bro')
 * const ResourceModel = require('./resource-model')
 * const AdminBroOptions = {
 *   resources: [{
 *     resource: ResourceModel
 *     options: {
 *       properties: {
 *         name: {
 *           components: {
 *             show: AdminBro.bundle('./my-react-component'),
 *           },
 *         },
 *       },
 *     },
 *   }],
 * }
 * ```
 *
 * In the example above we are altering how **name** property will look
 * like on the Show action. We can define **my-react-component.jsx** like this:
 *
 * ```
 * import React from 'react'
 * import { InputGroup, Label } from '@admin-bro/design-system'
 *
 * const MyReactComponent = props => {
 *   const { record, property } = props
 *   const value = record.params[property.name]
 *   return (
 *     <InputGroup>
 *       <Label>{property.name}</Label>
 *       {value} [meters]
 *     </InputGroup>
 *   )
 * }
 * ```
 *
 * @component
 * @name BasePropertyComponent
 * @subcategory Application
 * @example
 * const booleanProperty = {
 *   isTitle: false,
 *   name: 'awesome',
 *   isId: false,
 *   position: -1,
 *   label: 'I am awesome',
 *   type: 'boolean',
 * }
 *
 * const stringProperty = {
 *   isTitle: true,
 *   name: 'name',
 *   isId: false,
 *   position: -1,
 *   label: 'Name of a user',
 *   type: 'string',
 * }
 * // Resource is taken from the database
 * const resource = {
 *   id: 'User',
 *   name: 'User Model',
 *   titleProperty: 'name',
 *   resourceActions: [],
 *   listProperties: [booleanProperty, stringProperty],
 *   editProperties: [booleanProperty, stringProperty],
 *   showProperties: [booleanProperty, stringProperty],
 *   filterProperties: [booleanProperty, stringProperty],
 * }
 *
 * const initialRecord = {
 *   id: '1',
 *   title: 'John',
 *   params: {
 *     name: 'John',
 *     gender: 'male',
 *   },
 *   errors: {},
 *   recordActions: [],
 * }
 * const Wrapper = () => {
 *   const { record, handleChange, submit } = useRecord(initialRecord, resource.id)
 *   const params = JSON.stringify(record.params)
 *   return (
 *     <Box py="lg">
 *       <BasePropertyComponent
 *         property={booleanProperty}
 *         resource={resource}
 *         onChange={handleChange}
 *         where="edit"
 *         record={record}
 *       />
 *       <BasePropertyComponent
 *         property={stringProperty}
 *         resource={resource}
 *         onChange={handleChange}
 *         where="edit"
 *         record={record}
 *       />
 *      <Box>
 *        <Label>Params:</Label>
 *        {params}
 *      </Box>
 *      <Box my="lg">
 *        <Button variant="primary" onClick={submit}>Submit</Button>
 *        <Text variant="sm">
 *          This will throw an error because there is no AdminBro instance running
 *        </Text>
 *      </Box>
 *     </Box>
 *   )
 * }
 *
 * return (<Wrapper />)
 */
export default class BasePropertyComponent extends React.Component<BasePropertyProps> {
  static DefaultType

  static Boolean

  static DateTime

  static RichText

  static Reference

  static TextArea

  static Password

  render(): ReactNode {
    const { property, resource, record, filter, where, onChange } = this.props

    const testId = `property-${where}-${property.name}`

    let Component: ReactComponentLike = (types[property.type] && types[property.type][where])
    || defaultType[where]

    if (property.components && property.components[where]) {
      const component = property.components[where]
      if (!component) {
        throw new Error(`there is no "${property.name}.components.${where}"`)
      }
      Component = globalAny.AdminBro.UserComponents[component]
      return (
        <ErrorBoundary>
          <Box data-testid={testId}>
            <Component
              property={property}
              resource={resource}
              record={record}
              filter={filter}
              onChange={onChange}
              where={where}
            />
          </Box>
        </ErrorBoundary>
      )
    }

    const Array = ArrayType[where]
    const Mixed = MixedType[where]

    if (property.isArray) {
      if (!Array) { return (<div />) }
      return (
        <Array
          {...this.props}
          ItemComponent={BasePropertyComponent}
          testId={testId}
        />
      )
    }

    if (property.type === 'mixed' && property.subProperties && property.subProperties.length) {
      if (!Mixed) { return (<div />) }
      return (
        <Mixed
          {...this.props}
          ItemComponent={BasePropertyComponent}
          testId={testId}
        />
      )
    }

    return (
      <ErrorBoundary>
        <Box data-testid={testId}>
          <Component
            property={property}
            resource={resource}
            record={record}
            filter={filter}
            onChange={onChange}
            where={where}
          />
        </Box>
      </ErrorBoundary>
    )
  }
}


function camelizePropertyType<T>(type: {[key: string]: T}): {[key: string]: T} {
  return {
    Edit: type.edit,
    Show: type.show,
    List: type.list,
    Filter: type.filter,
  }
}

BasePropertyComponent.DefaultType = camelizePropertyType(defaultType)
BasePropertyComponent.Boolean = camelizePropertyType(boolean)
BasePropertyComponent.DateTime = camelizePropertyType(datetime)
BasePropertyComponent.RichText = camelizePropertyType(richtext)
BasePropertyComponent.Reference = camelizePropertyType(reference)
BasePropertyComponent.TextArea = camelizePropertyType(textarea)
BasePropertyComponent.Password = camelizePropertyType(password)