Source

adminjs/src/backend/utils/layout-element-parser/layout-element-parser.ts

/* eslint-disable max-len */
import {
  BoxProps,
  HeaderProps,
  TextProps,
  BadgeProps,
  ButtonProps,
  LinkProps,
  LabelProps,
  IconProps,
} from '@adminjs/design-system'
import { PropsWithChildren } from 'react'
import { CurrentAdmin } from '../../../current-admin.interface'

export type LayoutElement =
  string |
  Array<string> |
  Array<LayoutElement> |
 [string, PropsWithChildren<BoxProps>] |
 [PropsWithChildren<BoxProps>, Array<LayoutElement>] |
 ['@Header', PropsWithChildren<HeaderProps>] |
 ['@H1', PropsWithChildren<HeaderProps>] |
 ['@H2', PropsWithChildren<HeaderProps>] |
 ['@H3', PropsWithChildren<HeaderProps>] |
 ['@H4', PropsWithChildren<HeaderProps>] |
 ['@H5', PropsWithChildren<HeaderProps>] |
 ['@Text', PropsWithChildren<TextProps>] |
 ['@Badge', PropsWithChildren<BadgeProps>] |
 ['@Button', PropsWithChildren<ButtonProps>] |
 ['@Link', PropsWithChildren<LinkProps>] |
 ['@Label', PropsWithChildren<LabelProps>] |
 ['@Icon', PropsWithChildren<IconProps>] |
 [string, PropsWithChildren<any>]

/**
 * Function returning Array<LayoutElement> used by {@link Action#layout}
 *
 * @return  {Array<LayoutElement>}
 * @memberof Action
 * @alias LayoutElementFunction
 */
export type LayoutElementFunction = (currentAdmin?: CurrentAdmin) => Array<LayoutElement>

/**
 * It is generated from {@link Array<LayoutElement>} passed in {@link Action#layout}
 *
 * @alias ParsedLayoutElement
 * @memberof ActionJSON
 */
export type ParsedLayoutElement = {
  /** List of paths to properties which should be rendered by given element */
  properties: Array<string>;
  /** props passed to React component which wraps elements */
  props: PropsWithChildren<any>;
  /** Nested layout elements */
  layoutElements: Array<ParsedLayoutElement>;
  /** Component which should be used as a wrapper */
  component: string;
}

const isProp = (element): boolean => !!element
  && typeof element === 'object'
  && !Array.isArray(element)

const isComponentTag = (layoutElement: LayoutElement): boolean => (
  Array.isArray(layoutElement)
    && typeof layoutElement[0] === 'string'
    && layoutElement[0].startsWith('@')
    && isProp(layoutElement[1])
)

const hasOnlyStringsProperties = function (
  layoutElement: LayoutElement,
): layoutElement is [string] {
  return Array.isArray(layoutElement)
    && layoutElement.length > 0
    && !isComponentTag(layoutElement)
    && !(layoutElement as Array<any>).find(el => (typeof el !== 'string'))
}

const hasArrayOfLayoutElements = function (
  layoutElement: LayoutElement,
): layoutElement is [LayoutElement] {
  return Array.isArray(layoutElement)
    && layoutElement.length > 0
    && !(layoutElement as Array<any>).find(element => !Array.isArray(element))
}

const hasFirstStringProperty = function (layoutElement: LayoutElement): boolean {
  return Array.isArray(layoutElement)
    && typeof layoutElement[0] === 'string'
    && !isComponentTag(layoutElement)
}

const getPropertyNames = (layoutElement: LayoutElement): Array<string> => {
  if (typeof layoutElement === 'string') { return [layoutElement] }

  if (hasOnlyStringsProperties(layoutElement)) {
    return layoutElement
  }
  if (hasFirstStringProperty(layoutElement)) {
    return [layoutElement[0]] as Array<string>
  }
  return []
}

const getInnerLayoutElements = (layoutElement: LayoutElement): Array<LayoutElement> => {
  // only cases like [{}, layoutElement] (whatever follows props)
  if (Array.isArray(layoutElement)
    && isProp(layoutElement[0])) {
    return layoutElement[1] as Array<LayoutElement>
  }
  if (hasArrayOfLayoutElements(layoutElement)) {
    return layoutElement
  }
  return []
}

const getComponent = (layoutElement: LayoutElement): string => {
  if (isComponentTag(layoutElement)) {
    return (layoutElement[0] as string).slice(1)
  }
  return 'Box'
}

const getProps = (layoutElement: LayoutElement): BoxProps => {
  if (Array.isArray(layoutElement) && layoutElement.length) {
    const boxProps = (layoutElement as Array<any>).find(isProp)
    return boxProps as unknown as BoxProps || {}
  }
  return {}
}

export const layoutElementParser = (layoutElement: LayoutElement): ParsedLayoutElement => {
  const props = getProps(layoutElement)
  const innerLayoutElements = getInnerLayoutElements(layoutElement)
  const properties = getPropertyNames(layoutElement)
  const component = getComponent(layoutElement)

  return {
    props,
    layoutElements: innerLayoutElements.map(el => layoutElementParser(el)),
    properties,
    component,
  }
}

export default layoutElementParser


/**
 * @load layout-element.doc.md
 * @name LayoutElement
 * @typedef {String | Array} LayoutElement
 * @memberof Action
 * @alias LayoutElement
 */