Source

helpers/gridHelpers.js



/** @module GridHelpers */
import React from 'react';
import { TextField } from '@mui/material';
import { dateFormatter, currencyFormatter, dateStringNormalizer, caseless } from './helpers.js';
/**
* This is the base config for a column that is used by the MUIGrid component
* It takes a column from the layout and converts it into a config that can be used by the MUIGrid component
* @function
* @param {LayoutColumn} column - The column from the layout
* @returns {Object} - The column config for the MUIGrid component
* @see https://mui.com/components/data-grid/columns/
*/
export const baseColumnConfig = (layoutColumn, nullValue) => {

  let retCol = {
    field: layoutColumn.path || layoutColumn.render.name,
    fieldID: layoutColumn?.model?.field?.id,
    headerName: layoutColumn.render.label,
    headerClassName: 'pam-grid-header',
    source: layoutColumn,
  };

  if (layoutColumn.path && layoutColumn.path.includes('.') || layoutColumn.type == 10) {

    //We are a subfield, so we need to use the valueGetter to get the value
    retCol.valueGetter = (params) => {
      let path = layoutColumn.path.split('.');
      //Only return the top level value
      return params.row[path[0]];
    };

    // If the path includes a . then we need to dig down into the value so we will need to define a custom filterOperator
    // The input component will need to be a text field
    let filterOperator = {
      label: 'Contains',
      value: 'contains',
      InputComponent: ({ item, applyValue, ...rest }) => {
        return <TextField
          label="Value"
          inputRef={input => input && input.focus()}
          variant="standard" {...rest}
          value={item.value}
          placeholder="Filter value"
          onChange={(e) => applyValue({ ...item, value: e.target.value })}
        />;
      },
      getApplyFilterFn: (filterItem) => {
        return (params) => {
          if (!filterItem.value && filterItem.value !== 0) return true;
          let path = layoutColumn.path.split('.');
          let val_ = params?.row;
          for (const element of path) {
            val_ = val_ != null ? val_[element] : null;
          }

          // if the value is an array we need map the names to a string
          if (Array.isArray(val_)) {
            val_ = val_.filter(v => v !== null && v!== undefined).map(v => v?.name).join(' ');
          }

          // If the value is an object then we need to get the name
          if (typeof val_ === 'object') {
            val_ = val_?.name;
          }

          return val_ != null ? val_.toString().toLowerCase().includes(filterItem.value.toLowerCase()) : false;
        };
      },
    };
    retCol.filterOperators = [filterOperator];
  }

  // If the layout column has flex set then set the flex property
  if (layoutColumn.flex != undefined && layoutColumn.flex != null) {
    retCol.flex = layoutColumn.flex;

    // Additionally if the width is set then set the minWidth property
    if (layoutColumn.width != undefined && layoutColumn.width != null) {
      retCol.minWidth = layoutColumn.width;
    }
  } else {
    // Otherwise if the width is set then set the width property
    if (layoutColumn.width != undefined && layoutColumn.width != null) {
      retCol.width = layoutColumn.width;
    }
  }

  // If the layout column has a hidden property set to true then set the hide property
  if (layoutColumn.hidden) {
    retCol.hide = true;
  }

  retCol.nullValue = layoutColumn.nullValue || nullValue;

  return retCol;
};

/**
   * This function provides a date value or a default value if the date is null or undefined
   * It is used to render the cell as 'N/A' if the date is null or undefined
   * @function
   * @param {Date|String} value
   * @param {String} defaultValue
   * @returns {object}
   */
export const getDateOrDefault = (value, defaultValue) => {

  if (!value)
    return defaultValue;

  const val = value instanceof Date ? value : new Date(dateStringNormalizer(value));

  // If the date is invalid, return null
  if (isNaN(val.getTime())) {
    return defaultValue;
  }

  return val;
};

/**
 * This function provides a date value or a default value if the date is null or undefined.
 * The returned date is formatted as a string according to the common dateFormatter for the entire app.
 * @function
 * @param {Date|String} value
 * @param {String} defaultValue
 * @returns {String}
 * @see dateFormatter
 * @see getDateOrDefault
 */
export const getDateOrDefaultFormatted = (value, defaultValue) => {
  const val = getDateOrDefault(value, null);
  if (val) return dateFormatter(val);
  return defaultValue;
};

/**
 * This provides a name value or a default value if the name is null or undefined
 * It is used to render the cell as 'N/A' if the object or name is null or undefined
 * @function
 * @param {Object} value - The object that contains the name
 * @param {String} defaultValue - The default value to return if the name is null or undefined
 * @returns {String}
 */
export const getValueNameOrDefault = (value, defaultValue) => {
  if (!value) {
    return defaultValue;
  }

  // If its a list of objects get all their names and join them
  if (Array.isArray(value)) {
    return value.map(v => v?.name).join(', ');
  }

  return value.name || defaultValue;
};


/**
 * This function provides the basic formatting. We do not need to provide a getter or formatter for basic fields
 * If the editable flag is set to true then we will set the editable property to true and provide a valueSetter
 * @function
 * @param {Object} muiGridColumn
 */
export const addBasicFormatting = (muiGridColumn, editable) => {
  if (editable) {
    muiGridColumn.editable = true;
    muiGridColumn.valueSetter = ({ value, row }) => {
      // This does not work on the 'id' column BTW
      row[muiGridColumn.field] = value;
      return row;
    };
  }
}

/**
   * This takes a mui column and adds formatting to it to handle date fields
   * @function
   * @param {Object} muiGridColumn - The column from the layout
   * @see https://mui.com/components/data-grid/filtering/#value-getter
   * @see https://mui.com/components/data-grid/filtering/#value-options
   */
export const addDateFormatting = (muiGridColumn, editable) => {
  muiGridColumn.type = 'date';
  muiGridColumn.valueGetter = ({ value }) => getDateOrDefault(value, null); // Value GETTER needs to return null for the date to be displayed as N/A and for the filter to work
  muiGridColumn.valueFormatter = ({ value }) => getDateOrDefaultFormatted(value, muiGridColumn.nullValue);

  if (editable) {
    muiGridColumn.editable = true;
    muiGridColumn.valueSetter = ({ value, row }) => {
      // Update the row
      // This does not work on the 'id' column BTW
      row[muiGridColumn.field] = value;
      return row;
    };
  }
};

/**
 * @function addCurrencyFormatting
 * @param {object} muiGridColumn - The muiGridColumn to add the formatting to
 */
export const addCurrencyFormatting = (muiGridColumn) => {
  muiGridColumn.type = 'number';
  muiGridColumn.valueFormatter = ({ value }) => {
    if (value == null) return muiGridColumn.nullValue;
    return currencyFormatter(value);
  };
};


/**
   * This takes a mui column and adds formatting to it to handle single select fields.
   * It extracts the possible values from the layout column and adds them to the column config as choices
   * The filter uses these choices to filter the data
   * @function
   * @param {Object} muiGridColumn - The column used by the MUIGrid component
   * @param {Object} layoutColumn - The column from the layout
   * @see https://mui.com/components/data-grid/filtering/#value-getter
   * @see https://mui.com/components/data-grid/filtering/#value-options
   */
export const addSingleSelectFormatting = (muiGridColumn, layoutColumn, editable) => {
  // single select
  if (layoutColumn.render.choices?.length > 0) {
    muiGridColumn.type = 'singleSelect';
    muiGridColumn.valueOptions = layoutColumn.render.choices.map(c => {
      if (!c) return { value: null, label: null };
      return { value: c.label || c.name, label: c.label || c.name };
    });
  } else {
    console.log('No choices for column', layoutColumn);
    muiGridColumn.type = 'string';
  }
  muiGridColumn.valueGetter = ({ value }) => getValueNameOrDefault(value, muiGridColumn.nullValue);

  if (editable) {
    muiGridColumn.editable = true;
    muiGridColumn.valueSetter = ({ value, row }) => {
      // Update the row
      // Re-map the value to the object
      const mapped = layoutColumn.render.choices?.find(c => c?.label === value);
      if (mapped) {
        row[muiGridColumn.field] = mapped.source;
      }
      return row;
    };
  }
};

/**
 * This takes a mui column and adds formatting to it to handle object reference fields
 * It generates a link to the object reference
 * @function
 * @param {Object} muiGridColumn - The column used by the MUIGrid component
 * @param {Object} linkFormat - The column from the layout - TODO: This isnt true as we destructured the linkFormat from the layout column
 */
const addObjectReferenceFormatting = (muiGridColumn, { path }) => {
  if (path) {
    muiGridColumn.valueFormatter = ({ value }) => {

      let path_parts = path.split('.');

      //Remove the first element from the array
      path_parts.shift();

      for (const element of path_parts) {
        value = value != null ? value[element] : null;
      }

      // If the result is an object use the getNameOrDefault function to get the name or N/A
      if (value && typeof value === 'object') {
        return getValueNameOrDefault(value, muiGridColumn.nullValue);
      }

      // If the result is a string use the string or N/A
      if (value && typeof value === 'string') {
        return value;
      }
    };
  } else {
    muiGridColumn.valueFormatter = ({ value }) => getValueNameOrDefault(value, muiGridColumn.nullValue);
    muiGridColumn.valueGetter = ({ value }) => getValueNameOrDefault(value, muiGridColumn.nullValue);
  }

  muiGridColumn.type = 'string';
  muiGridColumn.sortComparator = (a, b) => caseless(getValueNameOrDefault(a), getValueNameOrDefault(b));
};

/**
 * This takes a mui column and adds formatting to it to handle external link fields
 * It generates a link to the external link
 * @function addExternalLinkFormatting
 * @param {Object} muiGridColumn - The column used by the MUIGrid component
 * @see https://mui.com/components/data-grid/rendering/#cell-renderers
 */
export const addExternalLinkFormatting = (muiGridColumn) => {

};

/**
 * This takes a mui column and adds formatting to it to handle action button fields
 * @function
 * @param {object} muiGridColumn - The column used by the MUIGrid component
 * @param {ActionData} actionData - The action data from the layout
 */
const addActionButtonFormatting = (muiGridColumn, actionData) => {
  // Get actions
  const actions = actionData?.actionList || [];
  actions.sort((a, b) => a.order - b.order);

};

/**
 * @typedef {Object} LayoutColumn
 * @property {string} path - The path to the data for the column
 * @property {number} type - The type of the column
 * @property {string} width - The width of the column
 * @property {string} [minWidth] - The value to display if the column is null
 * @property {number} [flex]  - The flex value for the column
 * @property {string} [nullValue] - The value to display if the column is null
 * @property {function} [columnOverride] - A function that takes a layout column and returns a mui column
 */

/**
 * This function takes a layout column and returns a mui column
 * It will determine the type of column and add formatting to the mui column
 * @function convertLayoutColumnToMuiColumn
 * @param {LayoutColumn} column
 * @returns {MuiGridColumn}
 */
export const convertLayoutColumnToMuiColumn = (column, nullValue, editable) => {
  let ret = baseColumnConfig(column, nullValue);

  if (column.columnOverride && typeof column.columnOverride === 'function') {
    return column.columnOverride(column, ret);
  }

  switch (column.type) {
    case 0: // Short Text
    case 1: // Long Text // These two fields dont need any special formatting
    case 2: // Integer
    case 3: // Float
      addBasicFormatting(ret, editable);
      break;
    case 4: // Currency
      addCurrencyFormatting(ret, editable); break;
    case 5: addDateFormatting(ret, editable); break; // Date
    case 6: // Flag
      break;
    case 7: addSingleSelectFormatting(ret, column, editable); break; // Single Select
    case 10: addObjectReferenceFormatting(ret, column, editable); break; // Object Link
    case 99: addActionButtonFormatting(ret, column.render); break; // Action Buttons
    case 100: addExternalLinkFormatting(ret); break; // Link
    default: console.error('Unknown column type', column.type); break;
  }

  return ret;
};