438 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
			
		
		
	
	
			438 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
/////////////////////////////////////////////////////////////
 | 
						|
//
 | 
						|
// pgAdmin 4 - PostgreSQL Tools
 | 
						|
//
 | 
						|
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
 | 
						|
// This software is released under the PostgreSQL Licence
 | 
						|
//
 | 
						|
//////////////////////////////////////////////////////////////
 | 
						|
 | 
						|
import { useCallback, useContext, useMemo, useState } from 'react';
 | 
						|
import _ from 'lodash';
 | 
						|
import PropTypes from 'prop-types';
 | 
						|
 | 
						|
import {
 | 
						|
  FormButton, FormInputCheckbox, FormInputColor, FormInputDateTimePicker,
 | 
						|
  FormInputFileSelect, FormInputKeyboardShortcut, FormInputQueryThreshold,
 | 
						|
  FormInputSQL, FormInputSelect, FormInputSelectThemes, FormInputSwitch,
 | 
						|
  FormInputText, FormInputToggle, FormNote, InputCheckbox, InputDateTimePicker,
 | 
						|
  InputFileSelect, InputRadio, InputSQL,InputSelect, InputSwitch, InputText,
 | 
						|
  InputTree, PlainString,
 | 
						|
} from 'sources/components/FormComponents';
 | 
						|
import { SelectRefresh } from 'sources/components/SelectRefresh';
 | 
						|
import Privilege from 'sources/components/Privilege';
 | 
						|
import { useIsMounted } from 'sources/custom_hooks';
 | 
						|
import CustomPropTypes from 'sources/custom_prop_types';
 | 
						|
import { evalFunc } from 'sources/utils';
 | 
						|
 | 
						|
import { SchemaStateContext } from './SchemaState';
 | 
						|
import { isValueEqual } from './common';
 | 
						|
import {
 | 
						|
  useFieldOptions, useFieldValue, useFieldError, useSchemaStateSubscriber,
 | 
						|
} from './hooks';
 | 
						|
import { listenDepChanges } from './utils';
 | 
						|
import { InputColor } from '../components/FormComponents';
 | 
						|
 | 
						|
 | 
						|
/* Control mapping for form view */
 | 
						|
function MappedFormControlBase({
 | 
						|
  id, type, state, onChange, className, inputRef, visible,
 | 
						|
  withContainer, controlGridBasis, noLabel, ...props
 | 
						|
}) {
 | 
						|
  let name = id;
 | 
						|
  const onTextChange = useCallback((e) => {
 | 
						|
    let val = e;
 | 
						|
    if(e?.target) {
 | 
						|
      val = e.target.value;
 | 
						|
    }
 | 
						|
    onChange?.(val);
 | 
						|
  }, []);
 | 
						|
  const value = state;
 | 
						|
 | 
						|
  const onSqlChange = useCallback((changedValue) => {
 | 
						|
    onChange?.(changedValue);
 | 
						|
  }, []);
 | 
						|
 | 
						|
  const onTreeSelection = useCallback((selectedValues)=> {
 | 
						|
    onChange?.(selectedValues);
 | 
						|
  }, []);
 | 
						|
 | 
						|
  if (!visible) {
 | 
						|
    return <></>;
 | 
						|
  }
 | 
						|
 | 
						|
  if (name && _.isNumber(name)) {
 | 
						|
    name = String(name);
 | 
						|
  }
 | 
						|
 | 
						|
  /* The mapping uses Form* components as it comes with labels */
 | 
						|
  switch (type) {
 | 
						|
  case 'int':
 | 
						|
    return <FormInputText
 | 
						|
      name={name} value={value} onChange={onTextChange} className={className}
 | 
						|
      inputRef={inputRef} {...props} type='int'
 | 
						|
    />;
 | 
						|
  case 'numeric':
 | 
						|
    return <FormInputText
 | 
						|
      name={name} value={value} onChange={onTextChange} className={className}
 | 
						|
      inputRef={inputRef} {...props} type='numeric'
 | 
						|
    />;
 | 
						|
  case 'tel':
 | 
						|
    return <FormInputText
 | 
						|
      name={name} value={value} onChange={onTextChange} className={className}
 | 
						|
      inputRef={inputRef} {...props} type='tel'
 | 
						|
    />;
 | 
						|
  case 'text':
 | 
						|
    return <FormInputText
 | 
						|
      name={name} value={value} onChange={onTextChange} className={className}
 | 
						|
      inputRef={inputRef} {...props}
 | 
						|
    />;
 | 
						|
  case 'multiline':
 | 
						|
    return <FormInputText
 | 
						|
      name={name} value={value} onChange={onTextChange} className={className}
 | 
						|
      inputRef={inputRef} controlProps={{ multiline: true }} {...props}
 | 
						|
    />;
 | 
						|
  case 'password':
 | 
						|
    return <FormInputText
 | 
						|
      name={name} value={value} onChange={onTextChange} className={className}
 | 
						|
      type='password' inputRef={inputRef} {...props}
 | 
						|
    />;
 | 
						|
  case 'select':
 | 
						|
    return <FormInputSelect
 | 
						|
      name={name} value={value} onChange={onTextChange} className={className}
 | 
						|
      inputRef={inputRef} {...props}
 | 
						|
    />;
 | 
						|
  case 'select-refresh':
 | 
						|
    return <SelectRefresh
 | 
						|
      name={name} value={value} onChange={onTextChange} className={className}
 | 
						|
      {...props}
 | 
						|
    />;
 | 
						|
  case 'switch':
 | 
						|
    return <FormInputSwitch
 | 
						|
      name={name} value={value} className={className}
 | 
						|
      onChange={(e) => onTextChange(e.target.checked, e.target.name)}
 | 
						|
      withContainer={withContainer} controlGridBasis={controlGridBasis}
 | 
						|
      {...props}
 | 
						|
    />;
 | 
						|
  case 'checkbox':
 | 
						|
    return <FormInputCheckbox
 | 
						|
      name={name} value={value} className={className}
 | 
						|
      onChange={(e) => onTextChange(e.target.checked, e.target.name)}
 | 
						|
      {...props}
 | 
						|
    />;
 | 
						|
  case 'toggle':
 | 
						|
    return <FormInputToggle
 | 
						|
      name={name} value={value} onChange={onTextChange} className={className}
 | 
						|
      inputRef={inputRef} {...props}
 | 
						|
    />;
 | 
						|
  case 'color':
 | 
						|
    return <FormInputColor
 | 
						|
      name={name} value={value} onChange={onTextChange} className={className}
 | 
						|
      {...props}
 | 
						|
    />;
 | 
						|
  case 'file':
 | 
						|
    return <FormInputFileSelect
 | 
						|
      name={name} value={value} onChange={onTextChange} className={className}
 | 
						|
      inputRef={inputRef} {...props}
 | 
						|
    />;
 | 
						|
  case 'sql':
 | 
						|
    return <FormInputSQL
 | 
						|
      name={name} value={value} onChange={onSqlChange} className={className}
 | 
						|
      noLabel={noLabel} inputRef={inputRef} {...props}
 | 
						|
    />;
 | 
						|
  case 'note':
 | 
						|
    return <FormNote className={className} {...props} />;
 | 
						|
  case 'datetimepicker':
 | 
						|
    return <FormInputDateTimePicker
 | 
						|
      name={name} value={value} onChange={onTextChange} className={className}
 | 
						|
      {...props}
 | 
						|
    />;
 | 
						|
  case 'keyboardShortcut':
 | 
						|
    return <FormInputKeyboardShortcut
 | 
						|
      name={name} value={value} onChange={onTextChange} {...props}
 | 
						|
    />;
 | 
						|
  case 'threshold':
 | 
						|
    return <FormInputQueryThreshold
 | 
						|
      name={name} value={value} onChange={onTextChange} {...props}
 | 
						|
    />;
 | 
						|
  case 'theme':
 | 
						|
    return <FormInputSelectThemes
 | 
						|
      name={name} value={value} onChange={onTextChange} {...props}
 | 
						|
    />;
 | 
						|
  case 'button':
 | 
						|
    return <FormButton
 | 
						|
      name={name} value={value} className={className} onClick={props.onClick}
 | 
						|
      {...props}
 | 
						|
    />;
 | 
						|
  case 'tree':
 | 
						|
    return <InputTree
 | 
						|
      name={name} treeData={props.treeData} onChange={onTreeSelection}
 | 
						|
      {...props}
 | 
						|
    />;
 | 
						|
  default:
 | 
						|
    return <PlainString value={value} {...props} />;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
MappedFormControlBase.propTypes = {
 | 
						|
  type: PropTypes.oneOfType([
 | 
						|
    PropTypes.string, PropTypes.func,
 | 
						|
  ]).isRequired,
 | 
						|
  state: PropTypes.any,
 | 
						|
  id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
 | 
						|
  onChange: PropTypes.func,
 | 
						|
  className: PropTypes.oneOfType([
 | 
						|
    PropTypes.string, PropTypes.object,
 | 
						|
  ]),
 | 
						|
  visible: PropTypes.bool,
 | 
						|
  inputRef: CustomPropTypes.ref,
 | 
						|
  noLabel: PropTypes.bool,
 | 
						|
  onClick: PropTypes.func,
 | 
						|
  withContainer: PropTypes.bool,
 | 
						|
  controlGridBasis: PropTypes.number,
 | 
						|
  treeData: PropTypes.oneOfType([
 | 
						|
    PropTypes.array, PropTypes.instanceOf(Promise), PropTypes.func]
 | 
						|
  ),
 | 
						|
};
 | 
						|
 | 
						|
/* Control mapping for grid cell view */
 | 
						|
function MappedCellControlBase({
 | 
						|
  cell, value, id, optionsLoaded, onCellChange, visible, reRenderRow, inputRef,
 | 
						|
  ...props
 | 
						|
}) {
 | 
						|
  let name = id;
 | 
						|
  const onTextChange = useCallback((e) => {
 | 
						|
    let val = e;
 | 
						|
    if (e?.target) {
 | 
						|
      val = e.target.value;
 | 
						|
    }
 | 
						|
 | 
						|
    onCellChange?.(val);
 | 
						|
  }, []);
 | 
						|
 | 
						|
  const onRadioChange = useCallback((e) => {
 | 
						|
    let val =e;
 | 
						|
    if(e?.target) {
 | 
						|
      val = e.target.checked;
 | 
						|
    }
 | 
						|
    onCellChange?.(val);
 | 
						|
  });
 | 
						|
 | 
						|
  const onSqlChange = useCallback((val) => {
 | 
						|
    onCellChange?.(val);
 | 
						|
  }, []);
 | 
						|
 | 
						|
  /* Some grid cells are based on options selected in other cells.
 | 
						|
   * lets trigger a re-render for the row if optionsLoaded
 | 
						|
   */
 | 
						|
  const optionsLoadedRerender = useCallback((res) => {
 | 
						|
    /* optionsLoaded is called when select options are fetched */
 | 
						|
    optionsLoaded?.(res);
 | 
						|
    reRenderRow?.();
 | 
						|
  }, []);
 | 
						|
 | 
						|
  if (!visible) {
 | 
						|
    return <></>;
 | 
						|
  }
 | 
						|
 | 
						|
  if (name && _.isNumber(name)) {
 | 
						|
    name = String('name');
 | 
						|
  }
 | 
						|
 | 
						|
  /* The mapping does not need Form* components as labels are not needed for grid cells */
 | 
						|
  switch(cell) {
 | 
						|
  case 'int':
 | 
						|
    return <InputText name={name} value={value} onChange={onTextChange} ref={inputRef} {...props} type='int'/>;
 | 
						|
  case 'numeric':
 | 
						|
    return <InputText name={name} value={value} onChange={onTextChange} ref={inputRef} {...props} type='numeric'/>;
 | 
						|
  case 'text':
 | 
						|
    return <InputText name={name} value={value} onChange={onTextChange} ref={inputRef} {...props}/>;
 | 
						|
  case 'password':
 | 
						|
    return <InputText name={name} value={value} onChange={onTextChange} ref={inputRef} {...props} type='password'/>;
 | 
						|
  case 'select':
 | 
						|
    return <InputSelect name={name} value={value} onChange={onTextChange} optionsLoaded={optionsLoadedRerender}
 | 
						|
      inputRef={inputRef} {...props}/>;
 | 
						|
  case 'switch':
 | 
						|
    return <InputSwitch name={name} value={value}
 | 
						|
      onChange={(e)=>onTextChange(e.target.checked, e.target.name)} {...props} />;
 | 
						|
  case 'checkbox':
 | 
						|
    return <InputCheckbox name={name} value={value}
 | 
						|
      onChange={(e)=>onTextChange(e.target.checked, e.target.name)} {...props} />;
 | 
						|
  case 'privilege':
 | 
						|
    return <Privilege name={name} value={value} onChange={onTextChange} {...props}/>;
 | 
						|
  case 'datetimepicker':
 | 
						|
    return <InputDateTimePicker name={name} value={value} onChange={onTextChange} {...props}/>;
 | 
						|
  case 'sql':
 | 
						|
    return <InputSQL name={name} value={value} onChange={onSqlChange} {...props} />;
 | 
						|
  case 'color':
 | 
						|
    return <InputColor name={name} value={value} onChange={onTextChange} {...props} />;
 | 
						|
  case 'file':
 | 
						|
    return <InputFileSelect name={name} value={value} onChange={onTextChange} inputRef={props.inputRef} {...props} />;
 | 
						|
  case 'keyCode':
 | 
						|
    return <InputText name={name} value={value} onChange={onTextChange} {...props} type='text' maxlength={1} />;
 | 
						|
  case 'radio':
 | 
						|
    return <InputRadio name={name} value={value} onChange={onRadioChange} disabled={props.disabled} {...props} ></InputRadio>;
 | 
						|
  default:
 | 
						|
    return <PlainString value={value} {...props} />;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
MappedCellControlBase.propTypes = {
 | 
						|
  cell: PropTypes.oneOfType([
 | 
						|
    PropTypes.string, PropTypes.func,
 | 
						|
  ]).isRequired,
 | 
						|
  value: PropTypes.any,
 | 
						|
  id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
 | 
						|
  onChange: PropTypes.func,
 | 
						|
  reRenderRow: PropTypes.func,
 | 
						|
  optionsLoaded: PropTypes.func,
 | 
						|
  onCellChange: PropTypes.func,
 | 
						|
  visible: PropTypes.bool,
 | 
						|
  disabled: PropTypes.bool,
 | 
						|
  inputRef: CustomPropTypes.ref,
 | 
						|
};
 | 
						|
 | 
						|
const ALLOWED_PROPS_FIELD_COMMON = [
 | 
						|
  'mode', 'value', 'readonly', 'disabled', 'hasError', 'id',
 | 
						|
  'label', 'options', 'optionsLoaded', 'controlProps', 'schema', 'inputRef',
 | 
						|
  'visible', 'autoFocus', 'helpMessage', 'className', 'optionsReloadBasis',
 | 
						|
  'orientation', 'isvalidate', 'fields', 'radioType', 'hideBrowseButton',
 | 
						|
  'btnName', 'hidden', 'withContainer', 'controlGridBasis', 'hasCheckbox',
 | 
						|
  'treeData', 'labelTooltip'
 | 
						|
];
 | 
						|
 | 
						|
const ALLOWED_PROPS_FIELD_FORM = [
 | 
						|
  'type', 'onChange', 'state', 'noLabel', 'text','onClick'
 | 
						|
];
 | 
						|
 | 
						|
const ALLOWED_PROPS_FIELD_CELL = [
 | 
						|
  'cell', 'onCellChange', 'reRenderRow', 'validate', 'disabled',
 | 
						|
  'readonly', 'radioType', 'hideBrowseButton', 'hidden', 'row',
 | 
						|
];
 | 
						|
 | 
						|
export const StaticMappedFormControl = ({accessPath, field, ...props}) => {
 | 
						|
  const schemaState = useContext(SchemaStateContext);
 | 
						|
  const state = schemaState.value(accessPath);
 | 
						|
  const newProps = {
 | 
						|
    ...props,
 | 
						|
    state,
 | 
						|
    noLabel: field.isFullTab,
 | 
						|
    ...field,
 | 
						|
    onChange: () => { /* Do nothing */ },
 | 
						|
  };
 | 
						|
  const visible = evalFunc(null, field.visible, state);
 | 
						|
 | 
						|
  if (visible === false) return <></>;
 | 
						|
 | 
						|
  return useMemo(
 | 
						|
    () => <MappedFormControlBase
 | 
						|
      {
 | 
						|
        ..._.pick(
 | 
						|
          newProps,
 | 
						|
          _.union(ALLOWED_PROPS_FIELD_COMMON, ALLOWED_PROPS_FIELD_FORM)
 | 
						|
        )
 | 
						|
      }
 | 
						|
    />, []
 | 
						|
  );
 | 
						|
};
 | 
						|
 | 
						|
StaticMappedFormControl.propTypes = {
 | 
						|
  accessPath: PropTypes.array.isRequired,
 | 
						|
  field: PropTypes.object,
 | 
						|
};
 | 
						|
 | 
						|
export const MappedFormControl = ({
 | 
						|
  accessPath, dataDispatch, field, onChange, ...props
 | 
						|
}) => {
 | 
						|
  const checkIsMounted = useIsMounted();
 | 
						|
  const [key, setKey] = useState(0);
 | 
						|
  const subscriberManager = useSchemaStateSubscriber(setKey);
 | 
						|
  const schemaState = useContext(SchemaStateContext);
 | 
						|
  const state = schemaState.data;
 | 
						|
  const value = useFieldValue(accessPath, schemaState, subscriberManager);
 | 
						|
  const options = useFieldOptions(accessPath, schemaState, subscriberManager);
 | 
						|
  const {hasError} = useFieldError(accessPath, schemaState, subscriberManager);
 | 
						|
  const avoidRenderingWhenNotMounted = (...args) => {
 | 
						|
    if (checkIsMounted()) subscriberManager.current?.signal(...args);
 | 
						|
  };
 | 
						|
 | 
						|
  const origOnChange = onChange;
 | 
						|
 | 
						|
  onChange = (changedValue) => {
 | 
						|
    if (!origOnChange || !checkIsMounted()) return;
 | 
						|
 | 
						|
    // We don't want the 'onChange' to be executed for the same value to avoid
 | 
						|
    // rerendering of the control, top component may still be rerendered on the
 | 
						|
    // change of the value.
 | 
						|
    const currValue = schemaState.value(accessPath);
 | 
						|
 | 
						|
    if (!isValueEqual(changedValue, currValue)) origOnChange(changedValue);
 | 
						|
  };
 | 
						|
 | 
						|
  const depVals = listenDepChanges(
 | 
						|
    accessPath, field, schemaState, avoidRenderingWhenNotMounted
 | 
						|
  );
 | 
						|
 | 
						|
  let newProps = {
 | 
						|
    ...props,
 | 
						|
    state: value,
 | 
						|
    noLabel: field.isFullTab,
 | 
						|
    ...field,
 | 
						|
    onChange: onChange,
 | 
						|
    dataDispatch: dataDispatch,
 | 
						|
    ...options,
 | 
						|
    hasError,
 | 
						|
  };
 | 
						|
 | 
						|
  if (typeof (field.type) === 'function') {
 | 
						|
    const typeProps = evalFunc(null, field.type, state);
 | 
						|
    newProps = {
 | 
						|
      ...newProps,
 | 
						|
      ...typeProps,
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  let origOnClick = newProps.onClick;
 | 
						|
  newProps.onClick = ()=>{
 | 
						|
    origOnClick?.();
 | 
						|
  };
 | 
						|
 | 
						|
  // FIXME:: Get this list from the option registry.
 | 
						|
  const memDeps = ['disabled', 'visible', 'readonly'].map(
 | 
						|
    option => options[option]
 | 
						|
  );
 | 
						|
 | 
						|
  memDeps.push(value);
 | 
						|
  memDeps.push(hasError);
 | 
						|
  memDeps.push(key);
 | 
						|
  memDeps.push(JSON.stringify(accessPath));
 | 
						|
  memDeps.push(depVals);
 | 
						|
 | 
						|
  // Filter out garbage props if any using ALLOWED_PROPS_FIELD.
 | 
						|
  return useMemo(
 | 
						|
    () => <MappedFormControlBase
 | 
						|
      {
 | 
						|
        ..._.pick(
 | 
						|
          newProps,
 | 
						|
          _.union(ALLOWED_PROPS_FIELD_COMMON, ALLOWED_PROPS_FIELD_FORM)
 | 
						|
        )
 | 
						|
      }
 | 
						|
    />, [...memDeps]
 | 
						|
  );
 | 
						|
};
 | 
						|
 | 
						|
MappedFormControl.propTypes = {
 | 
						|
  accessPath: PropTypes.array.isRequired,
 | 
						|
  field: PropTypes.object,
 | 
						|
  id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
 | 
						|
};
 | 
						|
 | 
						|
export const MappedCellControl = (props) => {
 | 
						|
  const newProps = _.pick(
 | 
						|
    props, _.union(ALLOWED_PROPS_FIELD_COMMON, ALLOWED_PROPS_FIELD_CELL)
 | 
						|
  );;
 | 
						|
 | 
						|
  // Filter out garbage props if any using ALLOWED_PROPS_FIELD.
 | 
						|
  return <MappedCellControlBase {...newProps}/>;
 | 
						|
};
 |