///////////////////////////////////////////////////////////// // // pgAdmin 4 - PostgreSQL Tools // // Copyright (C) 2013 - 2025, The pgAdmin Development Team // This software is released under the PostgreSQL Licence // ////////////////////////////////////////////////////////////// import React, { 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 ; case 'numeric': return ; case 'tel': return ; case 'text': return ; case 'multiline': return ; case 'password': return ; case 'select': return ; case 'select-refresh': return ; case 'switch': return onTextChange(e.target.checked, e.target.name)} withContainer={withContainer} controlGridBasis={controlGridBasis} {...props} />; case 'checkbox': return onTextChange(e.target.checked, e.target.name)} {...props} />; case 'toggle': return ; case 'color': return ; case 'file': return ; case 'sql': return ; case 'note': return ; case 'datetimepicker': return ; case 'keyboardShortcut': return ; case 'threshold': return ; case 'theme': return ; case 'button': return ; case 'tree': return ; default: return ; } } 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 ; case 'numeric': return ; case 'text': return ; case 'password': return ; case 'select': return ; case 'switch': return onTextChange(e.target.checked, e.target.name)} {...props} />; case 'checkbox': return onTextChange(e.target.checked, e.target.name)} {...props} />; case 'privilege': return ; case 'datetimepicker': return ; case 'sql': return ; case 'color': return ; case 'file': return ; case 'keyCode': return ; case 'radio': return ; default: return ; } } 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( () => , [] ); }; 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( () => , [...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 ; };