All Downloads are FREE. Search and download functionalities are using the official Maven repository.

components.form.utils.field-utils.ts Maven / Gradle / Ivy

import { getPlaceholder } from 'components/form/utils/input-utils';
import { determineBasedOnWidth } from 'components/form/utils/task-utils';
import { Indices } from 'contexts/FieldInfoContext';
import { FormInfo } from 'contexts/FormInfoContext';
import { TraitHelper } from 'helpers/traits';
import _ from 'lodash';
import { FieldInfo, FieldRenderType } from 'types/field';
import { Aux, AuxProps, Field, Trait, TraitName } from 'types/graphql';
import { TaskRendering } from 'types/theming';
import Types from 'types/types';
import { getPath, isEmptyValue } from 'utils/utils';

export const fieldMinWidth = 235
export function getDefaultValue(fieldType: FieldRenderType) {
  switch (fieldType) {
    case "TEXT":
    case "DECIMAL":
    case "INTEGER":
      return ""

    case "MULTIPLE SELECT":
    case "CHECKBOX SELECT":
      return []

    case "SINGLE SELECT":
    case "SINGLE SELECT SUBMIT":
    case "SEGMENTED BOOLEAN BUTTON":
    case "RADIO SELECT":
    case "RADIO BOOLEAN SELECT":
      return null

    case 'MULTIPLE':
      return [{}]

    case 'DATETIME':
    case 'TIME':
    case 'DATE':
      return null

    default:
      return ""
  }
}

function getValue(props: any) {
  const value = props.augProps.value
  if (value !== null && value !== undefined)
    return props.augProps.value

  return getDefaultValue(props.info.type)
}

export async function focusOnInputByClassName(className: string) {
  const elements = document.querySelectorAll(`.${className} input`)
    
  if (elements.length > 0){
    // @ts-ignore
    window.setTimeout(() => elements[0].focus(), 0);
  }
}

export async function focusOnField(rpath: string) {
  const element = document.getElementById(rpath);
  if (element){
    window.setTimeout(() => element.focus(), 0);
  } else {
    console.error("Could not focus on field with id: %o", rpath)
  }
}

// get all properties relevant to the rendering of an input field
export const toFieldProps = (props: any) => {
  const { t, augProps, info } = props
  const isEmpty               = (msg: string) => isEmptyValue(msg) || (_.isString(msg) && msg.trim().length === 0)
  const msg                   = augProps.error
  const helperText            = info.inMultiple ? undefined : isEmpty(msg) ? ' ' : msg // value ' ' creates a whitespace line for the helpertext component

  const inputFieldProps = info.field.type === "MULTIPLE" ? {} : {
    // tabIndex      : augProps.meta ? augProps.meta.tabindex : -1,
    onChange      : augProps.handleChange,
    onBlur        : augProps.handleBlur,
    onFocus       : augProps.handleFocus,
    placeholder   : getPlaceholder(t, info ? info.type : augProps.type),
    ...augProps.inputProps,
  }

  return {
    id            : info.rpath,
    name          : info.rpath,
    value         : getValue(props),
    required      : !Boolean(info.field.optional),
    error         : Boolean(augProps.isError()),
    helperText    : helperText,
    ...inputFieldProps
  }
}

export function toValueType(value: any) {
  if (!isNaN(parseInt(value)))
    return "INTEGER"
  else if (!isNaN(parseFloat(value)))
    return "DECIMAL"
  else
    return "TEXT"
}

export function getField(formInfo: FormInfo, fieldPath: string): Field {
  const fields = formInfo.form.fields
  const field  = getPath(fields, fieldPath)

  if (!field) {
    console.error("Could not find input field with path: %o", fieldPath)
    return undefined as unknown as Field
  }

  return field
}

export function getAux(formInfo: FormInfo, auxPath: string): AuxProps | undefined {
  return getPath(formInfo?.form?.aux, auxPath) as AuxProps
}

export function getFieldInput(formInfo: FormInfo, path: string): Field | undefined {
  if (path.includes("_basedOn"))
    throw "Attempting to get input field with path: '" + path + "'"

  const fieldPath  = toFieldPath(formInfo, path)
  return getField(formInfo, fieldPath)
}

export function getFieldAux(formInfo: FormInfo, rpath: string){
  const xpath = toAuxPath(formInfo, rpath)
  return getAux(formInfo, xpath)
}

export function toFieldRenderType(formInfo: FormInfo, field: Field, taskRendering: TaskRendering): FieldRenderType {
  const path  = field.path
  const rpath = replaceIndexesWith(path, "[0]")
  const aux   = getAux(formInfo, rpath)
  
  const hasOptions        = field.hasChoices
  const inMultiple        = path.includes('.')
  const isDependingSelect = field.dependants?.length > 0 && hasOptions
  const numOptions        = field.numChoices || aux?.numChoices || 0
  const forceSelect       = TraitHelper.hasTrait(field, "select")
  const isSelect          = inMultiple || numOptions < 2 || numOptions > 3 || isDependingSelect || forceSelect

  switch (field.type) {
    case "MULTIPLE FILE": 
      return field.type as FieldRenderType

    case "MULTIPLE":
      return field.type

    case field.type.match(/MULTIPLE.*/)?.input:
      return isSelect ? "MULTIPLE SELECT" : "CHECKBOX SELECT"

    case "ENTITY":
    case "SELECT":
    case (hasOptions || isDependingSelect) && field.type:
      const hasSubmitTrait = TraitHelper.hasTrait(field, 'submit')
      const isSubmitField = Types.isSubmitField(formInfo, field) 
      if (hasSubmitTrait && !isSubmitField)
        console.error("Cannot render %o as a submit button.", rpath)

      return isSubmitField ? "SINGLE SELECT SUBMIT" : isSelect ?  "SINGLE SELECT" : "RADIO SELECT"

    case "BOOLEAN":
      if (inMultiple) 
        return taskRendering == 'standard' ? "SEGMENTED BOOLEAN BUTTON" : "SINGLE SELECT"
      else
        return "RADIO BOOLEAN SELECT"

    default:
      return field.type as FieldRenderType
  }
}

export function replaceIndexesWith(path: string, replacement: string) {
  return path.replace(/\[[^\[\]]*\]/g, replacement);
}

export function toFieldInfoPath(path: string) {
  // example:
  // one[2].two[3].tree => one.fields.two.fields.tree
  return path.replace(/\./g, ".fields.").replace(/\[[^\[\]]*\]/g, "");
}

export function toFormFieldInfo(formInfo: FormInfo, field: Field, indices: Indices, taskRendering: TaskRendering): FieldInfo {
  const rpath       = toValuePath(field.path, indices)
  const auxPath     = toAuxPath(formInfo, rpath)
  const fieldPath   = toFieldPath(formInfo, rpath)
  const verifyField = getField(formInfo, fieldPath)
  const aux         = getAux(formInfo, auxPath)
  const type        = toFieldRenderType(formInfo, field, taskRendering)
  
  if (verifyField?.path != field.path)
    console.error("There is a critical issue in creating field info.")
  
  return {
    field:        field,
    type:         type,

    indices:      indices,
    rpath:        rpath,
    auxPath:      auxPath,
    fieldPath:    fieldPath,

    hasOptions:   field.hasChoices,
    options:      field?.choices ?? aux?.choices,
    optionsKind:  field?.choicesKind ?? aux?.choicesKind,
    numOptions:   (field?.numChoices ?? aux?.numChoices) || 0,

    inMultiple:   rpath.includes('.'),
    required:     !!field?.optional,
    isDepending:  field.dependants?.length > 0,
  }
}

export function toValuePath(path: string, indices: Indices): string {
  return path.replace(/\$\w+/g, (matched: string): string => {
    const name = matched.substring(1)
    const index = indices[name]
    //console.log("renderPath(%o): name=%o, index=%o", path, name, index)
    return index.toString()
  });
}

// build a field path based on formInfo
export function toFieldPath(formInfo: FormInfo, path: string) {
  function buildPath(fields: Field[] | undefined, parts: any[]): string[] {
    if (!parts.length)
      return []

    const current = parts.shift()
    if (current && fields) {
      const name    = replaceIndexesWith(current, "")
      const index   = fields.findIndex(field => field.name == name)
      const field   = fields[index]
      const fpath   = `[${index}]` + (parts.length > 0 ? ".fields" : "")
      return [fpath, ...buildPath(field.fields, parts)]
    } else
      return []
  }

  try {
    return buildPath(formInfo.form.fields, path.split(".")).join(".")
  } catch (e) {
    console.error("Could not create field path for path: %o", path)
    throw e
  }
}

export function mergeDefaults(staticDefault: any, dynamicDefault: any) {
  const obj: any = {}
  for (const key in staticDefault) {
    const staticValue = staticDefault[key]
    const dynamicValue = dynamicDefault[key]
    if (dynamicValue === null || dynamicValue === undefined || dynamicValue === "") {
      obj[key] = staticValue
    } else if (staticValue !== undefined) {
      if (Types.isObject(dynamicValue)) {
        obj[key] = Types.isObject(staticValue) ? mergeDefaults(staticValue, dynamicValue) : dynamicValue
      } else if (Array.isArray(dynamicValue)) {
        // know that array in staticDefault is always length 1
        // since it's taken from the model
        obj[key] = dynamicValue.length == 0
                   ? staticValue
                   : dynamicValue.map(element => mergeDefaults(staticValue[0], element))
      }
    } else {
      obj[key] = dynamicValue
    }
  }
  return obj
}

export function enrichValues(values: any, additionalValues: any) {
  const obj: any = {}
  for (const key in values) {
    const value = values[key]
    if (Array.isArray(value)) {
      const additionalValueArray = additionalValues[key]
      obj[key] = value.map((element, index) => enrichValues(element, additionalValueArray[index]))
    } else {
      obj[key] = value
    }
  }
  for (const key in additionalValues) {
    const value = additionalValues[key]
    if (Array.isArray(value)) continue
    obj[key] = value
  }
  return obj
}

export function collectAdditionalValues(values: any) {
  const obj: any = {}
  for (const key in values) {
    const value = values[key]
    // starting with __ indicates an additional value, e.g. __basedOn
    if (key.startsWith("__")) {
      obj[key] = value
    } else if (Array.isArray(value)) {
      obj[key] = value.map(elem => collectAdditionalValues(elem))
    }
  }
  return obj
}

export function collectDefaultsFromFields(formInfo: FormInfo, rpath: string) {
  function walkPath(fields: Field[], parts: string[]): Field | undefined {
    const current   = parts.shift()!
    const name      = replaceIndexesWith(current, "")
    const field     = fields?.find(field => field.name == name)
    if (!parts.length) return field
    else {
      if (field === undefined || field.fields === undefined) return undefined
      return walkPath(field?.fields, parts)
    }
  }

  function defaultFromField(field: Field): any {
    if (field.type !== "MULTIPLE") {
      return field.default
    }
    return fieldsToObject(field.fields!)
  }

  function fieldsToObject(fields: Field[]): any {
    const obj: any  = {}
    fields.forEach(subField => {
      obj[subField.name] = subField.type === "MULTIPLE"
        ? [defaultFromField(subField)]
        : subField.default
    })
    return obj
  }

  const parts = rpath.split(".").filter(x => x)
  if (!parts.length) {
    return fieldsToObject(formInfo.form.fields)
  }
  const field = walkPath(formInfo.form.fields, parts)
  const obj = field === undefined ? {} : defaultFromField(field)
  return obj
}

export function collectDefaultsFromAux(aux: Aux): any {
  //console.log("collectDefaultsFromAux: aux=%o", aux)
  const obj: any = {}
  for (const key in aux) {
    const value = aux[key]
    if (value === null) {
      obj[key] = null
    } else if (Array.isArray(value)) {
      obj[key] = value.map(elem => collectDefaultsFromAux(elem))
    } else {
      obj[key] = "default" in value ? value.default : null
    }
  }
  return obj
}

// build a value path based on formInfo
export function toAuxPath(formInfo: FormInfo, rpath: string) {

  function buildPath(fields: Field[] | undefined, parts: any[] ): string[] {
    if (!parts.length)
      return []

    const current = parts.shift()
    const name    = replaceIndexesWith(current, "")
    const field   = fields?.find(field => field.name == name)
    if (field?.type === "MULTIPLE" && field?.mode === "open")
      return [replaceIndexesWith(current, "[0]"), ...buildPath(field.fields, parts)]
    else
      return [current, ...buildPath(field?.fields, parts)]
  }

  try {
    return buildPath(formInfo.form.fields, rpath.split(".").filter(x => x)).join(".")
  } catch (e) {
    console.error("Could not create field path for path: %o", rpath)
    throw e
  }
}

export function createDefaultValue(formInfo: FormInfo, values: any, path: string = "") {
  const nonEmpty = (value: any) => Types.isObject(value) && Object.keys(value).length > 0 || Array.isArray(value) && value.length > 0 || !Array.isArray(value) && Boolean(value)

  function getValueEntry(values: any, pwd: string, key: string)  {
    const vpath = pwd + (pwd ? "." : "") + key
    const field = getFieldInput(formInfo, vpath)
    const value: any = field?.hasOwnProperty("fields") && Array.isArray(values[key])
      ? getValues(values[key], vpath)
      : toDefaultFieldValue(formInfo, field)

    return nonEmpty(value) ? [key, value] : []
  }

  function getValueObject(values: any, pwd: string) {
    const entries = Object
      .keys(values)
      .filter(key => !key.startsWith('__'))
      .map(key => getValueEntry(values, pwd, key))
      .filter(entry => entry.length > 0)

    return Object.fromEntries(entries)
  }

  function getValues(values: any, pwd: string): any {
    if (Array.isArray(values))
      return values
        .map((value,index) => getValues(value, pwd +  "[" + index + "]"))
        .filter(nonEmpty)
    else
      return getValueObject(values, pwd)
  }

  return getValues(values ? values : formInfo.form.values, path ? path : "")
}

function toDefaultFieldValue(formInfo: FormInfo, field: Field | undefined) {
  if (!field)
    return null

  if (field.hasOwnProperty("fields"))
    throw "Default values not defined for multiple fields"

  if (field.optional)
    return null

  // if there is only one choice, and it is a required field, select that choice
  const aux = getFieldAux(formInfo, field.path)
  if ((field?.numChoices ?? aux?.numChoices) == 1) {
    const choices = field?.choices || aux?.choices
    if (choices !== undefined) {
      if (field.type.includes("MULTIPLE"))
        return [choices[0]]
      else
        return choices[0]
    }
  }

  return null
}

export const fieldMinWidthStyle = (formInfo: FormInfo, field: Field, taskRendering: TaskRendering) => ({ minWidth: toFieldMinWidth(formInfo, field, taskRendering).toString() + "px" })

export function toFieldMinWidth(formInfo: FormInfo, field: Field, taskRendering: TaskRendering): number {
  const type  = toFieldRenderType(formInfo, field, taskRendering)
  const width = determineFieldWidth(formInfo, field, type, taskRendering)
  return width
}

export function determineFieldWidth(formInfo: FormInfo, field: Field, type: FieldRenderType, taskRendering: TaskRendering): number {

  switch (type) {
    case "MULTIPLE":

      // compute width of field columns
      const basedOnPath  = replaceIndexesWith(field.path, "[0]") + "[0].__basedOn"
      const basedOn      = _.get(formInfo.form.values, basedOnPath)
      const basedOnWidth = determineBasedOnWidth(basedOn, true)

      const widthPerField  = field?.fields?.map(subfield => toFieldMinWidth(formInfo, subfield, taskRendering)) || []
      // @ts-ignore
      const fieldsWidth: number    = widthPerField.reduce((a,b) => a+b, 0)

      // compute width of action columns
      const actionWidth = field.mode == "closed" ? 0 : 170

      return fieldsWidth + basedOnWidth + actionWidth

    case "TEXT":                     return 235
    case "FILE":                     return 235
    case "MULTIPLE SELECT":          return 235
    case "SINGLE SELECT":            return 235
    case "SEGMENTED BOOLEAN BUTTON": return 0
    case 'DATETIME':                 return 200
    case 'TIME':                     return 120
    case 'DATE':                     return 150
    case 'INTEGER':                  return 100
    case 'DECIMAL':                  return 100
    case 'PERIOD':                   return 0
    default:                         return 235

  }
}








© 2015 - 2024 Weber Informatics LLC | Privacy Policy