codekingpro/portable-devtools
114k
1/////////////////////////////////////////////////////////////2//3// pgAdmin 4 - PostgreSQL Tools4//5// Copyright (C) 2013 - 2024, The pgAdmin Development Team6// This software is released under the PostgreSQL Licence7//8//////////////////////////////////////////////////////////////9 10import React, { useContext, useEffect, useRef, useState } from 'react';11import { Box, Tab, Tabs, Grid } from '@mui/material';12import { makeStyles } from '@mui/styles';13import _ from 'lodash';14import PropTypes from 'prop-types';15import clsx from 'clsx';16 17import { MappedFormControl } from './MappedControl';18import TabPanel from '../components/TabPanel';19import DataGridView from './DataGridView';20import { SCHEMA_STATE_ACTIONS, StateUtilsContext } from '.';21import { FormNote, InputSQL } from '../components/FormComponents';22import gettext from 'sources/gettext';23import { evalFunc } from 'sources/utils';24import CustomPropTypes from '../custom_prop_types';25import { useOnScreen } from '../custom_hooks';26import { DepListenerContext } from './DepListener';27import FieldSetView from './FieldSetView';28 29const useStyles = makeStyles((theme)=>({30 fullSpace: {31 padding: 0,32 height: '100%',33 overflow: 'hidden',34 },35 controlRow: {36 marginBottom: theme.spacing(1),37 },38 nestedTabPanel: {39 backgroundColor: theme.otherVars.headerBg,40 },41 nestedControl: {42 height: 'unset',43 },44 fullControl: {45 display: 'flex',46 flexDirection: 'column'47 },48 errorMargin: {49 /* Error footer space */50 paddingBottom: '36px !important',51 },52 sqlTabInput: {53 border: 0,54 },55 nonTabPanel: {56 padding: 0,57 background: 'inherit',58 },59 nonTabPanelContent: {60 height: 'unset'61 }62}));63 64/* Optional SQL tab */65function SQLTab({active, getSQLValue}) {66 const classes = useStyles();67 const [sql, setSql] = useState('Loading...');68 useEffect(()=>{69 let unmounted = false;70 if(active) {71 setSql('Loading...');72 getSQLValue().then((value)=>{73 if(!unmounted) {74 setSql(value);75 }76 });77 }78 return ()=>{unmounted=true;};79 }, [active]);80 81 return <InputSQL82 value={sql}83 options={{84 readOnly: true,85 }}86 readonly={true}87 className={classes.sqlTabInput}88 />;89}90 91SQLTab.propTypes = {92 active: PropTypes.bool,93 getSQLValue: PropTypes.func.isRequired,94};95 96export function getFieldMetaData(field, schema, value, viewHelperProps, onlyModeCheck=false) {97 let retData = {98 readonly: false,99 disabled: false,100 visible: true,101 editable: true,102 canAdd: true,103 canEdit: false,104 canDelete: true,105 modeSupported: true,106 canAddRow: true,107 };108 109 if(field.mode) {110 retData.modeSupported = (field.mode.indexOf(viewHelperProps.mode) > -1);111 }112 if(!retData.modeSupported) {113 return retData;114 }115 116 if(onlyModeCheck) {117 return retData;118 }119 120 let {visible, disabled, readonly, editable} = field;121 let verInLimit;122 123 if (_.isUndefined(viewHelperProps.serverInfo)) {124 verInLimit= true;125 } else {126 verInLimit = ((_.isUndefined(field.server_type) ? true :127 (viewHelperProps.serverInfo.type in field.server_type)) &&128 (_.isUndefined(field.min_version) ? true :129 (viewHelperProps.serverInfo.version >= field.min_version)) &&130 (_.isUndefined(field.max_version) ? true :131 (viewHelperProps.serverInfo.version <= field.max_version)));132 }133 134 retData.readonly = viewHelperProps.inCatalog || (viewHelperProps.mode == 'properties');135 if(!retData.readonly) {136 retData.readonly = evalFunc(schema, readonly, value);137 }138 139 let _visible = verInLimit;140 _visible = _visible && evalFunc(schema, _.isUndefined(visible) ? true : visible, value);141 retData.visible = Boolean(_visible);142 143 retData.disabled = Boolean(evalFunc(schema, disabled, value));144 145 retData.editable = !(viewHelperProps.inCatalog || (viewHelperProps.mode == 'properties'));146 if(retData.editable) {147 retData.editable = evalFunc(schema, _.isUndefined(editable) ? true : editable, value);148 }149 150 let {canAdd, canEdit, canDelete, canReorder, canAddRow } = field;151 retData.canAdd = _.isUndefined(canAdd) ? retData.canAdd : evalFunc(schema, canAdd, value);152 retData.canAdd = !retData.disabled && retData.canAdd;153 retData.canEdit = _.isUndefined(canEdit) ? retData.canEdit : evalFunc(schema, canEdit, value);154 retData.canEdit = !retData.disabled && retData.canEdit;155 retData.canDelete = _.isUndefined(canDelete) ? retData.canDelete : evalFunc(schema, canDelete, value);156 retData.canDelete = !retData.disabled && retData.canDelete;157 retData.canReorder =_.isUndefined(canReorder) ? retData.canReorder : evalFunc(schema, canReorder, value);158 retData.canAddRow = _.isUndefined(canAddRow) ? retData.canAddRow : evalFunc(schema, canAddRow, value);159 return retData;160}161 162/* The first component of schema view form */163export default function FormView({164 value, schema={}, viewHelperProps, isNested=false, accessPath, dataDispatch, hasSQLTab,165 getSQLValue, onTabChange, firstEleRef, className, isDataGridForm=false, isTabView=true, visible}) {166 let defaultTab = 'General';167 let tabs = {};168 let tabsClassname = {};169 const [tabValue, setTabValue] = useState(0);170 const classes = useStyles();171 const firstEleID = useRef();172 const formRef = useRef();173 const onScreenTracker = useRef(false);174 const depListener = useContext(DepListenerContext);175 let groupLabels = {};176 const schemaRef = useRef(schema);177 const stateUtils = useContext(StateUtilsContext);178 179 let isOnScreen = useOnScreen(formRef);180 181 useEffect(()=>{182 if(isOnScreen) {183 /* Don't do it when the form is alredy visible */184 if(!onScreenTracker.current) {185 /* Re-select the tab. If form is hidden then sometimes it is not selected */186 setTabValue((prev)=>prev);187 onScreenTracker.current = true;188 }189 } else {190 onScreenTracker.current = false;191 }192 }, [isOnScreen]);193 194 useEffect(()=>{195 /* Calculate the fields which depends on the current field */196 if(!isDataGridForm) {197 schemaRef.current.fields.forEach((field)=>{198 /* Self change is also dep change */199 if(field.depChange || field.deferredDepChange) {200 depListener.addDepListener(accessPath.concat(field.id), accessPath.concat(field.id), field.depChange, field.deferredDepChange);201 }202 (evalFunc(null, field.deps) || []).forEach((dep)=>{203 // when dep is a string then prepend the complete accessPath204 let source = accessPath.concat(dep);205 206 // but when dep is an array, then the intention is to provide the exact accesspath207 if(_.isArray(dep)) {208 source = dep;209 }210 if(field.depChange || field.deferredDepChange) {211 depListener.addDepListener(source, accessPath.concat(field.id), field.depChange, field.deferredDepChange);212 }213 if(field.depChange || field.deferredDepChange) {214 depListener.addDepListener(source, accessPath.concat(field.id), field.depChange, field.deferredDepChange);215 }216 });217 });218 return ()=>{219 /* Cleanup the listeners when unmounting */220 depListener.removeDepListener(accessPath);221 };222 }223 }, []);224 225 /* Upon reset, set the tab to first */226 useEffect(()=>{227 setTabValue(0);228 }, [stateUtils.formResetKey]);229 230 let fullTabs = [];231 let inlineComponents = [];232 let inlineCompGroup = null;233 234 /* Prepare the array of components based on the types */235 for(const field of schemaRef.current.fields) {236 let {visible, disabled, readonly, canAdd, canEdit, canDelete, canReorder, canAddRow, modeSupported} =237 getFieldMetaData(field, schema, value, viewHelperProps);238 239 if(modeSupported) {240 let {group, CustomControl} = field;241 if(field.type === 'group') {242 groupLabels[field.id] = field.label;243 if(!visible) {244 schemaRef.current.filterGroups.push(field.label);245 }246 continue;247 }248 group = groupLabels[group] || group || defaultTab;249 250 if(!tabs[group]) tabs[group] = [];251 252 /* Lets choose the path based on type */253 if(field.type === 'nested-tab') {254 /* Pass on the top schema */255 if(isNested) {256 field.schema.top = schemaRef.current.top;257 } else {258 field.schema.top = schema;259 }260 tabs[group].push(261 <FormView key={`nested${tabs[group].length}`} value={value} viewHelperProps={viewHelperProps}262 schema={field.schema} accessPath={accessPath} dataDispatch={dataDispatch} isNested={true} isDataGridForm={isDataGridForm}263 {...field} visible={visible}/>264 );265 } else if(field.type === 'nested-fieldset') {266 /* Pass on the top schema */267 if(isNested) {268 field.schema.top = schemaRef.current.top;269 } else {270 field.schema.top = schema;271 }272 tabs[group].push(273 <FieldSetView key={`nested${tabs[group].length}`} value={value} viewHelperProps={viewHelperProps}274 schema={field.schema} accessPath={accessPath} dataDispatch={dataDispatch} isNested={true} isDataGridForm={isDataGridForm}275 controlClassName={classes.controlRow}276 {...field} visible={visible}/>277 );278 } else if(field.type === 'collection') {279 /* If its a collection, let data grid view handle it */280 /* Pass on the top schema */281 if(isNested) {282 field.schema.top = schemaRef.current.top;283 } else {284 field.schema.top = schemaRef.current;285 }286 287 if(!_.isUndefined(field.fixedRows)) {288 canAdd = false;289 canDelete = false;290 }291 292 const props = {293 key: field.id, value: value[field.id] || [], viewHelperProps: viewHelperProps,294 schema: field.schema, accessPath: accessPath.concat(field.id), dataDispatch: dataDispatch,295 containerClassName: classes.controlRow, ...field, canAdd: canAdd, canReorder: canReorder,296 canEdit: canEdit, canDelete: canDelete,297 visible: visible, canAddRow: canAddRow, onDelete: field.onDelete, canSearch: field.canSearch,298 expandEditOnAdd: field.expandEditOnAdd,299 fixedRows: (viewHelperProps.mode == 'create' ? field.fixedRows : undefined),300 addOnTop: Boolean(field.addOnTop)301 };302 303 if(CustomControl) {304 tabs[group].push(<CustomControl {...props}/>);305 } else {306 tabs[group].push(<DataGridView {...props}/>);307 }308 } else {309 /* Its a form control */310 const hasError = _.isEqual(accessPath.concat(field.id), stateUtils.formErr.name);311 /* When there is a change, the dependent values can change312 * lets pass the new changes to dependent and get the new values313 * from there as well.314 */315 if(field.isFullTab) {316 tabsClassname[group] = classes.fullSpace;317 fullTabs.push(group);318 }319 320 const id = field.id || `control${tabs[group].length}`;321 if(visible && !disabled && !firstEleID.current) {322 firstEleID.current = field.id;323 }324 325 let currentControl = <MappedFormControl326 inputRef={(ele)=>{327 if(firstEleRef && firstEleID.current === field.id) {328 if(typeof firstEleRef == 'function') {329 firstEleRef(ele);330 } else {331 firstEleRef.current = ele;332 }333 }334 }}335 state={value}336 key={id}337 viewHelperProps={viewHelperProps}338 name={id}339 value={value[id]}340 {...field}341 id={id}342 readonly={readonly}343 disabled={disabled}344 visible={visible}345 onChange={(changeValue)=>{346 /* Get the changes on dependent fields as well */347 dataDispatch({348 type: SCHEMA_STATE_ACTIONS.SET_VALUE,349 path: accessPath.concat(id),350 value: changeValue,351 });352 }}353 hasError={hasError}354 className={classes.controlRow}355 noLabel={field.isFullTab}356 memoDeps={[357 value[id],358 readonly,359 disabled,360 visible,361 hasError,362 classes.controlRow,363 ...(evalFunc(null, field.deps) || []).map((dep)=>value[dep]),364 ]}365 />;366 367 if(field.isFullTab && field.helpMessage) {368 currentControl = (<React.Fragment key={`coll-${field.id}`}>369 <FormNote key={`note-${field.id}`} text={field.helpMessage}/>370 {currentControl}371 </React.Fragment>);372 }373 374 if(field.inlineNext) {375 inlineComponents.push(React.cloneElement(currentControl, {376 withContainer: false, controlGridBasis: 3377 }));378 inlineCompGroup = group;379 } else if(inlineComponents?.length > 0) {380 inlineComponents.push(React.cloneElement(currentControl, {381 withContainer: false, controlGridBasis: 3382 }));383 tabs[group].push(384 <Grid container spacing={0} key={`ic-${inlineComponents[0].key}`} className={classes.controlRow} rowGap="8px">385 {inlineComponents}386 </Grid>387 );388 inlineComponents = [];389 inlineCompGroup = null;390 } else {391 tabs[group].push(currentControl);392 }393 }394 }395 }396 397 if(inlineComponents?.length > 0) {398 tabs[inlineCompGroup].push(399 <Grid container spacing={0} key={`ic-${inlineComponents[0].key}`} className={classes.controlRow} rowGap="8px">400 {inlineComponents}401 </Grid>402 );403 }404 405 let finalTabs = _.pickBy(tabs, (v, tabName)=>schemaRef.current.filterGroups.indexOf(tabName) <= -1);406 407 /* Add the SQL tab if required */408 let sqlTabActive = false;409 let sqlTabName = gettext('SQL');410 if(hasSQLTab) {411 sqlTabActive = (Object.keys(finalTabs).length === tabValue);412 /* Re-render and fetch the SQL tab when it is active */413 finalTabs[sqlTabName] = [414 <SQLTab key="sqltab" active={sqlTabActive} getSQLValue={getSQLValue} />,415 ];416 tabsClassname[sqlTabName] = classes.fullSpace;417 fullTabs.push(sqlTabName);418 }419 420 useEffect(()=>{421 onTabChange?.(tabValue, Object.keys(tabs)[tabValue], sqlTabActive);422 }, [tabValue]);423 424 /* check whether form is kept hidden by visible prop */425 if(!_.isUndefined(visible) && !visible) {426 return <></>;427 }428 429 if(isTabView) {430 return (431 <Box height="100%" display="flex" flexDirection="column" className={className} ref={formRef} data-test="form-view">432 <Box>433 <Tabs434 value={tabValue}435 onChange={(event, selTabValue) => {436 setTabValue(selTabValue);437 }}438 variant="scrollable"439 scrollButtons="auto"440 action={(ref)=>ref?.updateIndicator()}441 >442 {Object.keys(finalTabs).map((tabName)=>{443 return <Tab key={tabName} label={tabName} data-test={tabName}/>;444 })}445 </Tabs>446 </Box>447 {Object.keys(finalTabs).map((tabName, i)=>{448 let contentClassName = [stateUtils.formErr.message ? classes.errorMargin : null];449 if(fullTabs.indexOf(tabName) == -1) {450 contentClassName.push(classes.nestedControl);451 } else {452 contentClassName.push(classes.fullControl);453 }454 return (455 <TabPanel key={tabName} value={tabValue} index={i} classNameRoot={clsx(tabsClassname[tabName], isNested ? classes.nestedTabPanel : null)}456 className={clsx(contentClassName)} data-testid={tabName}>457 {finalTabs[tabName]}458 </TabPanel>459 );460 })}461 </Box>);462 } else {463 let contentClassName = [classes.nonTabPanelContent, stateUtils.formErr.message ? classes.errorMargin : null];464 return (465 <Box height="100%" display="flex" flexDirection="column" className={clsx(className)} ref={formRef} data-test="form-view">466 <TabPanel value={tabValue} index={0} classNameRoot={classes.nonTabPanel}467 className={clsx(contentClassName)}>468 {Object.keys(finalTabs).map((tabName)=>{469 return (470 <React.Fragment key={tabName}>{finalTabs[tabName]}</React.Fragment>471 );472 })}473 </TabPanel>474 </Box>);475 }476}477 478FormView.propTypes = {479 value: PropTypes.any,480 schema: CustomPropTypes.schemaUI.isRequired,481 viewHelperProps: PropTypes.object,482 isNested: PropTypes.bool,483 isDataGridForm: PropTypes.bool,484 isTabView: PropTypes.bool,485 visible: PropTypes.oneOfType([486 PropTypes.bool, PropTypes.func,487 ]),488 accessPath: PropTypes.array.isRequired,489 dataDispatch: PropTypes.func,490 hasSQLTab: PropTypes.bool,491 getSQLValue: PropTypes.func,492 onTabChange: PropTypes.func,493 firstEleRef: CustomPropTypes.ref,494 className: CustomPropTypes.className,495};496 