///////////////////////////////////////////////////////////// // // pgAdmin 4 - PostgreSQL Tools // // Copyright (C) 2013 - 2025, The pgAdmin Development Team // This software is released under the PostgreSQL Licence // ////////////////////////////////////////////////////////////// import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { Box, Tab, Tabs } from '@mui/material'; import _ from 'lodash'; import PropTypes from 'prop-types'; import { FormFooterMessage, MESSAGE_TYPE, FormNote } from 'sources/components/FormComponents'; import TabPanel from 'sources/components/TabPanel'; import { useOnScreen } from 'sources/custom_hooks'; import CustomPropTypes from 'sources/custom_prop_types'; import gettext from 'sources/gettext'; import { FieldControl } from './FieldControl'; import { SQLTab } from './SQLTab'; import { FormContentBox } from './StyledComponents'; import { SchemaStateContext } from './SchemaState'; import { useFieldSchema, useFieldValue, useSchemaStateSubscriber, } from './hooks'; import { registerView, View } from './registry'; import { createFieldControls, listenDepChanges } from './utils'; import FormViewTab from './FormViewTab'; const ErrorMessageBox = () => { const [key, setKey] = useState(0); const schemaState = useContext(SchemaStateContext); const onErrClose = useCallback(() => { const err = { ...schemaState.errors, message: '' }; // Unset the error message, but not the name. schemaState.setError(err); }, [schemaState]); const errors = schemaState.errors; const message = errors?.message || ''; useEffect(() => { // Refresh on message changes. return schemaState.subscribe( ['errors', 'message'], () => setKey(Date.now()), 'states' ); }, [key]); return ; }; // The first component of schema view form. export default function FormView({ accessPath, schema=null, isNested=false, dataDispatch, className, hasSQLTab, getSQLValue, isTabView=true, viewHelperProps, field, showError=false, resetKey, focusOnFirstInput=false }) { const [key, setKey] = useState(0); const subscriberManager = useSchemaStateSubscriber(setKey); const schemaState = useContext(SchemaStateContext); const value = useFieldValue(accessPath, schemaState); const { visible } = useFieldSchema( field, accessPath, value, viewHelperProps, schemaState, subscriberManager ); const [tabValue, setTabValue] = useState(0); const formRef = useRef(); const onScreenTracker = useRef(false); let isOnScreen = useOnScreen(formRef); if (!schema) schema = field.schema; // Set focus on the first focusable element. useEffect(() => { if (!focusOnFirstInput) return; setTimeout(() => { const formEle = formRef.current; if (!formEle) return; const activeTabElement = formEle.querySelector( '[data-test="tabpanel"]:not([hidden])' ); if (!activeTabElement) return; // Find the first focusable input, which is either: // * An editable Input element. // * A select element, which is not disabled. // * An href element. // * Any element with 'tabindex', but - tabindex is not set to '-1'. const firstFocussableElement = activeTabElement.querySelector([ 'button:not([role="tab"])', '[href]', 'input:not(disabled)', 'select:not(disabled)', 'textarea', '[tabindex]:not([tabindex="-1"]):not([data-test="tabpanel"])', 'div[class="cm-content"]:not([aria-readonly="true"])', ].join(', ')); if (firstFocussableElement) firstFocussableElement.focus(); }, 200); }, [tabValue]); useEffect(() => { // Refresh on message changes. return subscriberManager.current?.add( schemaState, ['errors', 'message'], 'states', (newState, prevState) => { if (_.isUndefined(newState) || _.isUndefined(prevState)) subscriberManager.current?.signal(); } ); }, [key]); useEffect(() => { if (!visible) return; if(isOnScreen) { /* Don't do it when the form is alredy visible */ if(!onScreenTracker.current) { /* Re-select the tab. If form is hidden then sometimes it is not selected */ setTabValue((prev)=>prev); onScreenTracker.current = true; } } else { onScreenTracker.current = false; } }, [isOnScreen]); listenDepChanges( accessPath, field, schemaState, () => subscriberManager.current?.signal() ); // Upon reset, set the tab to first. useEffect(() => { if (!visible || !resetKey) return; if (resetKey) { setTabValue(0); } }, [resetKey]); const finalGroups = useMemo( () => createFieldControls({ schema, schemaState, accessPath, viewHelperProps, dataDispatch }), [schema, schemaState, accessPath, viewHelperProps, dataDispatch] ); // Check whether form is kept hidden by visible prop. if(!finalGroups || (!_.isUndefined(visible) && !visible)) { return <>; } const isSingleCollection = () => { const DataGridView = View('DataGridView'); return ( finalGroups.length == 1 && finalGroups[0].controls.length == 1 && finalGroups[0].controls[0].control == DataGridView ); }; if(isTabView) { return ( <> { setTabValue(nextTabIndex); }} variant="scrollable" scrollButtons="auto" action={(ref) => ref?.updateIndicator()} >{ finalGroups.map((tabGroup, idx) => ) }{hasSQLTab && } { finalGroups.map((group, idx) => { let contentClassName = [ group.isFullTab ? 'FormView-fullControl' : 'FormView-nestedControl', schemaState.errors?.message ? 'FormView-errorMargin' : null ]; let id = group.id.replace(' ', ''); return ( { group.isFullTab && group.field?.helpMessage ? : <> } { group.controls.map( (item, idx) => ) } ); }) } { hasSQLTab && } { showError && } ); } else { let contentClassName = [ isSingleCollection() ? 'FormView-singleCollectionPanelContent' : 'FormView-nonTabPanelContent', (schemaState.errors?.message ? 'FormView-errorMargin' : null), (finalGroups.some((g)=>g.isFullTab) ? 'FormView-fullControl' : ''), ]; return ( <> g.isFullTab) ? 'FormView-fullSpace' : ''), ].join(' ')} className={contentClassName.join(' ')}> { finalGroups.map((group, idx) => { group.controls.map( (item, idx) => ) } ) } { hasSQLTab && } { showError && } ); } } FormView.propTypes = { schema: CustomPropTypes.schemaUI, viewHelperProps: PropTypes.object, isNested: PropTypes.bool, isTabView: PropTypes.bool, accessPath: PropTypes.array.isRequired, dataDispatch: PropTypes.func, hasSQLTab: PropTypes.bool, getSQLValue: PropTypes.func, className: CustomPropTypes.className, field: PropTypes.object, showError: PropTypes.bool, resetKey: PropTypes.number, focusOnFirstInput: PropTypes.bool, }; registerView(FormView, 'FormView');