Source

helpers/viewLayout.js

import {
  CONDITIONAL_RENDER,
  EMAIL, PHONE, ZIP,
  FIELD_TYPES as FIELDS,
  STATIC_TYPES
} from '../constants.js';
import { currencyFormatter, dateFormatter, isEmpty, sortOn, isObject, formatPhoneNumber } from './helpers.js';

/**
 * Create an error section
 * @function
 * @param {string} title - title
 * @param {string} label - label
 * @param {string} message - message
 * @returns {object[]} - error section
 */
export const createErrorSection = (title, label, message) => {
  return [{
    name: title || 'Error',
    areas: [
      [
        { label: label || 'Issue', type: 'text', value: message || 'There is a problem with the layout of this view. Please contact your administrator.' }
      ]
    ]
  }];
};

/**
 * Parse a view field
 * @function
 * @param {object} layout - layout object
 * @param {object[]} layout.sections - sections array
 * @param {object} data - data object
 * @param {string} key - key
 * @returns {object[]} - parsed sections
 */
export const parseViewLayout = (layout, data, key) => {
  if (!layout || !layout.sections) {
    return createErrorSection();
  }

  if (!data) {
    return createErrorSection(
      'No Data',
      'Issue',
      'There is no data to display for this view. This may be a bad link or the data may not exist.'
    );
  }
  const sections = layout.sections.map((section) => parseSection(section, data, key)).filter((section) => section);
  return sections;
};

/**
 * Parse a section
 * @function
 * @param {object} section - section object
 * @param {object} data - data object
 * @param {string} key - key
 * @returns {object} - parsed section
 */
export const parseSection = (section, data, key) => {
  const { layout } = section;
  const areas = [];

  layout.forEach((area, index) => {
    // check if the element is an array
    const areaFields = [];
    if (Array.isArray(area)) {
      // if it is, then we need to parse it as a row
      area.forEach(element => {
        // if it is, then we need to parse it as a row
        const field = parseViewField(element, data, key);
        if (field) {
          areaFields.push(field);
        }
      });
    } else {
      // if it is not, then we need to parse it as a field
      const field = parseViewField(area, data, key);
      if (field) {
        areaFields.push(field);
      }
    }
    if (areaFields.length) {
      areas.push(areaFields);
    }
  });

  const hasNonStaticFields = areas.some((area) => {
    if (Array.isArray(area)) {
      return area.some((field) => !valueExists(field.type, STATIC_TYPES));
    }
    return !valueExists(area.type, STATIC_TYPES);
  });

  return hasNonStaticFields ? { name: section.name, areas, columns: !!section.columns } : null;
}

/**
 * Check if a value is in the object
 * @function
 * @param {string | number} value - value to check
 * @returns {boolean} true if value is in the object
 */
export const valueExists = (value, obj) => Object.values(obj).includes(value);

/**
 * Parse a field
 * @function
 * @param {object} field - field object
 * @param {Map<string, string>} asyncFieldsMap - map of async fields
 * @returns {ParsedField} parsed field
 */
export function parseViewField(field, data, key, nested = false) {
  if (!field) {
    return null;
  }

  if (field.hideIf) {
    if (Array.isArray(field.hideIf)) {
      if (field.hideIf.includes(key)) {
        return null;
      }
    } else if (field.hideIf === key) {
      return null;
    }
  }

  const hidden = !!field[CONDITIONAL_RENDER.HIDDEN];
  if (hidden && !field.conditions) {
    return null;
  }

  const conditionalProps = getConditionalLoadout(field, data);
  // Check if the field's hidden has been changed by the conditional loadout
  if (conditionalProps?.hidden || (hidden && conditionalProps?.hidden !== false)) {
    return null;
  }

  delete conditionalProps.hidden;

  const { label, type, model, linkFormat, defaultValue } = field;
  const fieldId = model?.name || field.path;
  let name = fieldId || `unknown${model?.id || ''}`;
  let inData = data?.[name];

  // Check if the field is static and generate a unique id / name
  const isStatic = valueExists(type, STATIC_TYPES);
  if (isStatic || !fieldId) {
    inData = null;
    name = `${type}-${Date.now()}`;
  }

  const empty = defaultValue || 'N/A';

  let parsedField = {
    id: name,
    label,
    type,
    empty,
    className: field.className,
    ...conditionalProps,
  };

  if (valueExists(field.type, STATIC_TYPES)) {
    parsedField = { ...parsedField, ...getStaticProps(field, data) };
  } else if (!valueExists(field.type, FIELDS)) {
    console.warn(`Field type ${type} is not supported by the view layout`);
    return parsedField;
  }

  if (field.singleColumnSize) {
    parsedField.singleColumnSize = parseInt(field.singleColumnSize);
  }

  if (type === FIELDS.TEXT) {
    parsedField[EMAIL] = !!field[EMAIL];
    parsedField[PHONE] = !!field[PHONE];
    parsedField[ZIP] = !!field[ZIP];
  }

  if (nested) {
    return parsedField;
  }

  if (type === FIELDS.OBJECT && field.linkFormat) {
    parsedField.renderAsLinks = true;
    parsedField.linkFormat = linkFormat;
  }

  parsedField.value = getViewValue(field, inData, empty, key);
  parsedField.rawValue = inData;

  if (parsedField.value === empty && parsedField.renderAsLinks) {
    parsedField.renderAsLinks = false;
  }

  return parsedField;
}
/**
 * Get the value of the view view model
 * @function
 * @param {object} field - field object
 * @param {any} inData - data to parse
 * @param {string} empty - empty value
 * @param {string} key - key of the field
 * @returns {any} value of the view
 */
export const getViewValue = (field, inData, empty, key) => {
  const { type } = field;

  const extractName = (item) => item?.name || item?.label;
  let value = inData;
  if (isEmpty(inData)) {
    value = empty;
  } else {
    if (type === FIELDS.CHOICE || type === FIELDS.OBJECT) {
      // If data is an array get the label of each item
      if (!inData) {
        value = empty;
      } else if (Array.isArray(inData)) {
        if (type === FIELDS.OBJECT && field?.linkFormat) {
          value = inData;
        } else {
          value = inData.map(extractName).join(', ');
        }
      } else if (typeof inData === 'object') {
        if (type === FIELDS.OBJECT && field?.linkFormat) {
          value = inData;
        } else {
          value = extractName(inData);
        }
      }
    }

    if (type === FIELDS.INT) {
      value = parseInt(value);
    }

    if (type === FIELDS.FLOAT) {
      value = parseFloat(value);
    }

    if (type === FIELDS.CURRENCY) {
      value = currencyFormatter(value);
    }

    if (type === FIELDS.DATE) {
      value = dateFormatter(value);
    }

    if (type === FIELDS.TEXT || type === FIELDS.LONG_TEXT) {
      if (field[PHONE]) {
        value = formatPhoneNumber(value);
      }
    }

    // Special case for cluster fields
    if (type === FIELDS.CLUSTER) {
      const headers = [];
      const subFields = field.layout?.map((subF) => {
        const parsedField = parseViewField(subF, null, key, true);
        headers.push(parsedField.label);
        return parsedField;
      });
      // const subFields = field.layout?.map((subF) => {
      //   const { label, type, model, defaultValue: empty } = subF;
      //   const name = model?.name || `unknown${model?.id || ''}`;
      //   headers.push({label, id: name});
      //   return {id: name, label, type, empty};
      // });

      const rows = [];
      if (Array.isArray(inData)) {
        inData.forEach((item) => {
          const row = subFields.map((subF) => getViewValue(subF, item[subF.id], subF.empty, true));
          rows.push(row);
        });
      }

      value = { headers, rows };
      // Loop through the sub fields and parse them
      // This will also populate the validations property for each sub field
      // const subFields = field.layout?.map((subF) => parseViewField(subF, inData, key, true));
      // parsedField.subFields = subFields;
    }
  }

  return value;
}

/**
 * Parse and set various properties for supported static fields
 * @function getStaticProps
 * @param {object} field - field object
 * @returns {object} parsed field
 */
export function getStaticProps(field) {
  const { type } = field;

  const staticProps = {
    static: true
  };

  if (type === STATIC_TYPES.COMPONENT) {
    staticProps.component = field.component;
    staticProps.componentProps = field.componentProps || {};
  }

  if (type === STATIC_TYPES.IMAGE) {
    staticProps.src = field.src;
    staticProps.alt = field.alt;
  }

  if (type === STATIC_TYPES.TEXT || type === STATIC_TYPES.HEADER) {
    staticProps.text = field.text;
    staticProps.variant = field.variant;
  }

  return staticProps;
}

/**
 * Get the conditional loadout for a field
 * @function getViewFieldValue
 * @param {object} inData - data object
 * @returns {string | object[]} field value
 */
export function getViewFieldValue(inData) {
  if (Array.isArray(inData)) {
    return sortOn((inData)).map((con) => {
      if (isObject(con)) {
        return con?.id;
      }
      return con;
    });
  }

  if (isObject(inData)) {
    return inData?.id?.toString() || '';
  }

  return inData?.toString() || '';
}

/**
 * Check if a field is visible based on the conditions and data
 * @function getConditionalLoadout
 * @param {object} field - parsed field
 * @param {object} data - data object
 * @returns {object} returns the conditional loadout for the field
 */
export const getConditionalLoadout = (field, data) => {
  const { conditions } = field || {};
  let props = {}
  if (conditions?.length) {
    conditions.forEach((condition) => {
      const { when: triggerId, is: value, isValid, then: loadOut } = condition;
      let triggerData = data[triggerId];
      if (!isEmpty(triggerData)) {
        triggerData = getViewFieldValue(triggerData);
      }
      if (isValid || triggerData.toString() === value.toString()) {
        props = { ...props, ...loadOut };
      }
    });
  }
  return props;
};