ManageIQ/manageiq-ui-classic

View on GitHub
app/javascript/components/miq-data-table/miq-table-cell.jsx

Summary

Maintainability
D
1 day
Test Coverage
/* eslint-disable no-unused-expressions */
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import {
  Button, TableCell, TextInput, Toggle, Link,
} from 'carbon-components-react';
import classNames from 'classnames';
import MiqConfirmActionModal, { modalCallbackTypes } from '../miq-confirm-action-modal';
import {
  CellAction, hasIcon, hasImage, hasButton, hasTextInput, hasToggle, hasLink, isObject, isArray, isNumber, decimalCount,
} from './helper';
import { customOnClickHandler } from '../../helpers/custom-click-handler';
import { carbonizeIcon } from '../../menu/icon';

const MiqTableCell = ({
  cell, onCellClick, row, truncate,
}) => {
  const [confirm, setConfirm] = useState(false);

  const longText = truncate && ((cell.value).length > 40);
  const veryLongText = truncate && ((cell.value).length > 300);

  const truncateClass = longText ? 'truncate_cell' : '';
  const wrapClass = longText ? 'white_space_normal' : '';
  const alignClass = longText ? 'vertical_align_top' : '';
  const longerTextClass = veryLongText ? 'truncate_longer_text' : '';

  const truncateText = (
    <span title={cell.value} className={classNames('bx--front-line', wrapClass, longerTextClass)}>
      {cell.value}
    </span>
  );
  const cellClass = classNames('cell', truncateClass, alignClass, cell.data.style_class);
  const cellText = () => (
    <div className={cellClass}>
      {truncateText}
    </div>
  );

  /** Function to print a number with decimal. */
  const renderArrayListItem = (item) => {
    if (isNumber(item)) {
      return decimalCount(item) >= 2 ? item : parseFloat(item).toFixed(1);
    }
    return item;
  };

  /** Function to render a list within a table cell.
   * Usage eg: Overview / Chargeback / Rates / Item (Summary)
  */
  const cellArrayList = (data) => (
    data && data.text && (
      <div className="cell">
        <div className="array_list">
          {
            data.text.map((item, index) => (
              <div className={classNames('list_row')} key={index.toString()}>
                {renderArrayListItem(item)}
              </div>
            ))
          }
        </div>
      </div>
    )
  );

  /** Function to render an image in cell. */
  const cellImage = ({ image }) => {
    const altText = image.split('/').pop().split('.')[0];
    return (
      <div className={cellClass}>
        <img src={image} alt={altText} className="image" />
        {truncateText}
      </div>
    );
  };

  const returnIcon = (icon, style, styledIconClass, longerTextClass, index = undefined) => {
    const extraProps = {};
    if (index !== undefined) {
      extraProps.key = index.toString();
    }
    if (icon.startsWith('carbon--')) {
      const IconElement = carbonizeIcon(icon);
      return (
        <IconElement aria-label={icon} className={classNames('icon', 'carbon-icons-style', icon)} style={style} {...extraProps} />
      );
    }
    return (<i className={classNames('fa-lg', 'icon', icon, styledIconClass, longerTextClass)} style={style} {...extraProps} />);
  };

  /** Function to render icon(s) in cell. */
  const renderIcon = (icon, style, showText, title = '') => {
    const hasBackground = Object.keys(style).includes('background');
    const styledIconClass = hasBackground ? 'styled_icon' : '';
    const longerTextClass = hasBackground && veryLongText ? 'styled_icon_margin' : '';
    return (
      <div className={cellClass} title={title}>
        {
          typeof (icon) === 'string'
            ? returnIcon(icon, style, styledIconClass, longerTextClass)
            : icon.map((i, index) => returnIcon(i, style, styledIconClass, longerTextClass, index))
        }
        {showText && truncateText}
      </div>
    );
  };

  /** Function to render an icon in cell based on the 'type' in 'item'. */
  const cellIcon = (item, showText) => {
    if (showText) {
      const color = item.props ? item.props.style : {};
      const iconStyle = item.background ? { background: item.background, color: '#FFF' } : color;
      return renderIcon(item.icon, iconStyle, showText, item.title);
    }
    const { className, style } = item.props ? item.props : { className: item.icon, style: { color: '#000' } };
    return renderIcon(className, style, showText);
  };

  /** Function to execute a button click event
   * 'callbackAction' is added to row so that, the event can be executed within the parent component. eg: SettingsCompanyTags
  */
  const cellButtonEvent = (item, event) => {
    if (item.callback) {
      onCellClick({ ...row, callbackAction: item.callback }, CellAction.buttonCallback, event);
    } else if (item.onclick) {
      (item.onclick.confirm)
        ? setConfirm(true)
        : customOnClickHandler(item.onclick);
    }
  };

  /** Function to handle the confirmation-modal-box button-click events. */
  const confirmModalAction = (actionType, item) => {
    if (actionType === modalCallbackTypes.OK) {
      customOnClickHandler(item.onclick);
    }
    setConfirm(false);
  };

  /** Function to render a confirmation-modal-box. */
  const renderConfirmModal = (item) => {
    const modalData = {
      open: confirm,
      confirm: item.onclick.confirm,
      callback: (actionType) => confirmModalAction(actionType, item),
    };
    return <MiqConfirmActionModal modalData={modalData} />;
  };

  /** Function to render a Button inside cell. */
  /** Eg: Button was used for 'Services / Catalogs' */
  const cellButton = (item) => (
    <>
      <div className={cellClass}>
        <Button
          onClick={(e) => cellButtonEvent(item, e)}
          disabled={item.disabled}
          onKeyPress={(e) => cellButtonEvent(item, e)}
          tabIndex={0}
          size={item.size ? item.size : 'sm'}
          title={item.title ? item.title : truncateText}
          kind={item.kind ? item.kind : 'primary'}
          className={classNames('miq-data-table-button', item.buttonClassName)}
        >
          {truncateText}
        </Button>
      </div>
      {confirm && renderConfirmModal(item)}
    </>
  );

  /** Function to render a Text Box inside cell. */
  const cellTextInput = (item, id) => (
    <div className={cellClass}>
      <TextInput
        id={id}
        className={item.className}
        labelText={truncateText}
        placeholder={item.placeholder}
        defaultValue={item.value}
        invalid={item.invalid}
        invalidText={item.invalidText}
        type={item.type}
        readOnly={item.readonly}
        disabled={item.disabled}
        tabIndex={0}
        onChange={item.onchange}
      />
    </div>
  );

  /** Function to render a Toggle inside cell. */
  const cellToggle = (item, id) => (
    <div className={cellClass}>
      <Toggle
        id={id}
        labelText={truncateText}
        labelA={item.labelA}
        labelB={item.labelB}
        toggled={item.toggled}
        onToggle={() => (item.ontoggle() ? item.ontoggle() : undefined)}
        disabled={item.disabled}
        tabIndex={0}
      />
    </div>
  );

  /** Function to render a Link inside cell. */
  const cellLink = (item, _id) => (
    <div className={cellClass}>
      <Link href={item.href}>
        {truncateText}
      </Link>
    </div>
  );

  /** Determines which component has to be rendered inside a cell.
   * Also to determine if a click event necessary for a cell or its component . */
  const cellComponent = () => {
    const { data, id } = cell;
    const keys = Object.keys(data);
    const content = { component: '', cellClick: !!onCellClick, showText: true };
    if (isObject(data)) {
      if (isArray(data.text)) return { ...content, component: cellArrayList(data), cellClick: false };

      if (data.text && hasImage(keys, data)) return { ...content, component: cellImage(data) };

      const { showIcon, showText } = hasIcon(keys, data);
      if (showIcon) return { ...content, component: cellIcon(data, showText), showText };

      if (hasButton(keys)) return { ...content, component: cellButton(data), cellClick: false };

      if (hasToggle(keys)) return { ...content, component: cellToggle(data, id), cellClick: false };

      if (hasTextInput(keys)) return { ...content, component: cellTextInput(data, id), cellClick: false };

      if (hasLink(keys)) return { ...content, component: cellLink(data, id), cellClick: false };

      return { ...content, component: cellText() };
    }
    return { ...content, component: cellText() };
  };

  const { component, cellClick, showText } = cellComponent();

  return (
    <TableCell
      key={cell.id}
      onClick={(event) => cellClick && onCellClick(row, CellAction.itemClick, event)}
      className={classNames(showText ? '' : 'no_text', wrapClass ? 'vertical_align_top' : '', cell.data.actionCell ? 'action-cell-holder' : '')}
    >
      {component}
    </TableCell>
  );
};

export default MiqTableCell;

MiqTableCell.propTypes = {
  onCellClick: PropTypes.func,
  row: PropTypes.shape({
    actionCell: PropTypes.bool,
  }),
  cell: PropTypes.shape({
    id: PropTypes.string,
    value: PropTypes.string,
    // eslint-disable-next-line react/forbid-prop-types
    data: PropTypes.any,
    clickable: PropTypes.bool,
  }),
  truncate: PropTypes.bool,
};

MiqTableCell.defaultProps = {
  onCellClick: undefined,
  row: {},
  cell: {},
  truncate: true,
};