- Introduce deps listener context and move the old code. - Add support for deffered deps change - deferredDepChange. - Changes for canEditRow and canDeleteRow. - Select the tab again on visible if it is rendered when hidden. - It is helpful to show the error footer in SQL tab also. - Fix isEmptyString.

This commit is contained in:
Aditya Toshniwal 2021-07-15 14:00:02 +05:30 committed by Akshay Joshi
parent 5c1ce23780
commit a06f78b2d5
7 changed files with 336 additions and 115 deletions

View File

@ -9,7 +9,7 @@
/* The DataGridView component is based on react-table component */
import React, { useCallback, useMemo, useRef, useState } from 'react';
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { Box } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import { PgIconButton } from '../components/Buttons';
@ -28,6 +28,9 @@ import FormView from './FormView';
import { confirmDeleteRow } from '../helpers/legacyConnector';
import CustomPropTypes from 'sources/custom_prop_types';
import { evalFunc } from 'sources/utils';
import { useOnScreen } from '../custom_hooks';
import { DepListenerContext } from './DepListener';
import { getSchemaRow } from '../utils';
const useStyles = makeStyles((theme)=>({
grid: {
@ -57,6 +60,9 @@ const useStyles = makeStyles((theme)=>({
padding: 0,
minWidth: 0,
backgroundColor: 'inherit',
'&.Mui-disabled': {
border: 0,
},
},
gridTableContainer: {
overflow: 'auto',
@ -138,13 +144,33 @@ DataTableHeader.propTypes = {
headerGroups: PropTypes.array.isRequired,
};
function DataTableRow({row, totalRows, isResizing}) {
function DataTableRow({row, totalRows, isResizing, schema, accessPath}) {
const classes = useStyles();
const [key, setKey] = useState(false);
const depListener = useContext(DepListenerContext);
/* Memoize the row to avoid unnecessary re-render.
* If table data changes, then react-table re-renders the complete tables
* We can avoid re-render by if row data is not changed
*/
useEffect(()=>{
/* Calculate the fields which depends on the current field
deps has info on fields which the current field depends on. */
schema.fields.forEach((field)=>{
/* Self change is also dep change */
if(field.depChange) {
depListener.addDepListener(accessPath.concat(field.id), accessPath.concat(field.id), field.depChange);
}
(evalFunc(null, field.deps) || []).forEach((dep)=>{
let source = accessPath.concat(dep);
if(_.isArray(dep)) {
source = dep;
}
depListener.addDepListener(source, accessPath.concat(field.id), field.depChange);
});
});
}, []);
let depsMap = _.values(row.values, Object.keys(row.values).filter((k)=>!k.startsWith('btn')));
depsMap = depsMap.concat([totalRows, row.isExpanded, key, isResizing]);
return useMemo(()=>
@ -168,18 +194,7 @@ function DataTableRow({row, totalRows, isResizing}) {
export default function DataGridView({
value, viewHelperProps, formErr, schema, accessPath, dataDispatch, containerClassName, ...props}) {
const classes = useStyles();
/* Calculate the fields which depends on the current field
deps has info on fields which the current field depends on. */
const dependsOnField = useMemo(()=>{
let res = {};
schema.fields.forEach((field)=>{
(field.deps || []).forEach((dep)=>{
res[dep] = res[dep] || [];
res[dep].push(field.id);
});
});
return res;
}, []);
/* Using ref so that schema variable is not frozen in columns closure */
const schemaRef = useRef(schema);
let columns = useMemo(
@ -195,11 +210,17 @@ export default function DataGridView({
dataType: 'edit',
width: 30,
minWidth: '0',
Cell: ({row})=><PgIconButton data-test="expand-row" title={gettext('Edit row')} icon={<EditRoundedIcon />} className={classes.gridRowButton}
onClick={()=>{
row.toggleRowExpanded(!row.isExpanded);
}}
/>
Cell: ({row})=>{
let canEditRow = true;
if(props.canEditRow) {
canEditRow = evalFunc(schemaRef.current, props.canEditRow, row.original || {});
}
return <PgIconButton data-test="expand-row" title={gettext('Edit row')} icon={<EditRoundedIcon />} className={classes.gridRowButton}
onClick={()=>{
row.toggleRowExpanded(!row.isExpanded);
}} disabled={!canEditRow}
/>
}
};
colInfo.Cell.displayName = 'Cell',
colInfo.Cell.propTypes = {
@ -218,17 +239,23 @@ export default function DataGridView({
width: 30,
minWidth: '0',
Cell: ({row}) => {
let canDeleteRow = true;
if(props.canDeleteRow) {
canDeleteRow = evalFunc(schemaRef.current, props.canDeleteRow, row.original || {});
}
return (
<PgIconButton data-test="delete-row" title={gettext('Delete row')} icon={<DeleteRoundedIcon />}
onClick={()=>{
confirmDeleteRow(()=>{
/* Get the changes on dependent fields as well */
dataDispatch({
type: SCHEMA_STATE_ACTIONS.DELETE_ROW,
path: accessPath,
value: row.index,
});
}, ()=>{}, props.customDeleteTitle, props.customDeleteMsg);
}} className={classes.gridRowButton} />
}} className={classes.gridRowButton} disabled={!canDeleteRow} />
);
}
};
@ -287,25 +314,10 @@ export default function DataGridView({
disabled={!editable}
visible={_visible}
onCellChange={(value)=>{
/* Get the changes on dependent fields as well.
* The return value of depChange function is merged and passed to state.
*/
const depChange = (state)=>{
let rowdata = _.get(state, accessPath.concat(row.index));
_field.depChange && _.merge(rowdata, _field.depChange(rowdata, _field.id) || {});
(dependsOnField[_field.id] || []).forEach((d)=>{
d = _.find(schemaRef.current.fields, (f)=>f.id==d);
if(d.depChange) {
_.merge(rowdata, d.depChange(rowdata, _field.id) || {});
}
});
return state;
};
dataDispatch({
type: SCHEMA_STATE_ACTIONS.SET_VALUE,
path: accessPath.concat([row.index, _field.id]),
value: value,
depChange: depChange,
});
}}
reRenderRow={other.reRenderRow}
@ -326,12 +338,7 @@ export default function DataGridView({
);
const onAddClick = useCallback(()=>{
let newRow = {};
columns.forEach((column)=>{
if(column.field) {
newRow[column.field.id] = schemaRef.current.defaults[column.field.id];
}
});
let newRow = schemaRef.current.getNewData();
dataDispatch({
type: SCHEMA_STATE_ACTIONS.ADD_ROW,
path: accessPath,
@ -387,12 +394,12 @@ export default function DataGridView({
{rows.map((row, i) => {
prepareRow(row);
return <React.Fragment key={i}>
<DataTableRow row={row} totalRows={rows.length} canExpand={props.canEdit}
value={value} viewHelperProps={viewHelperProps} formErr={formErr} isResizing={isResizing}
schema={schemaRef.current} accessPath={accessPath.concat([row.index])} dataDispatch={dataDispatch} />
<DataTableRow row={row} totalRows={rows.length} isResizing={isResizing}
schema={schemaRef.current} accessPath={accessPath.concat([row.index])} />
{props.canEdit && row.isExpanded &&
<FormView value={row.original} viewHelperProps={viewHelperProps} formErr={formErr} dataDispatch={dataDispatch}
schema={schemaRef.current} accessPath={accessPath.concat([row.index])} isNested={true} className={classes.expandedForm}/>
schema={schemaRef.current} accessPath={accessPath.concat([row.index])} isNested={true} className={classes.expandedForm}
isDataGridForm={true}/>
}
</React.Fragment>;
})}

View File

@ -0,0 +1,77 @@
import _ from 'lodash';
import React from 'react';
export const DepListenerContext = React.createContext();
export default class DepListener {
constructor() {
this._depListeners = [];
}
/* Will keep track of the dependent fields and there callbacks */
addDepListener(source, dest, callback, defCallback) {
this._depListeners = this._depListeners || [];
this._depListeners.push({
source: source,
dest: dest,
callback: callback,
defCallback: defCallback
});
}
_getListenerData(state, listener, actionObj) {
/* Get data at same level */
let data = state;
let dataPath = _.slice(listener.dest, 0, -1);
if(dataPath.length > 0) {
data = _.get(state, dataPath);
}
data = _.assign(data, listener.callback && listener.callback(data, listener.source, state, actionObj) || {});
return state;
}
_getDefListenerPromise(state, listener, actionObj) {
/* Get data at same level */
let data = state;
let dataPath = _.slice(listener.dest, 0, -1);
if(dataPath.length > 0) {
data = _.get(state, dataPath);
}
return (listener.defCallback && listener.defCallback(data, listener.source, state, actionObj));
}
/* Called when any field changed and trigger callbacks */
getDepChange(currPath, state, actionObj) {
if(actionObj.depChangeResolved) {
state = this._getListenerData(state, {callback: actionObj.depChangeResolved}, actionObj);
} else {
let allListeners = _.filter(this._depListeners, (entry)=>_.join(currPath, '|').startsWith(_.join(entry.source, '|')));
if(allListeners) {
for(const listener of allListeners) {
state = this._getListenerData(state, listener, actionObj);
}
}
}
return state;
}
getDeferredDepChange(currPath, state, actionObj) {
let deferredList = [];
let allListeners = _.filter(this._depListeners, (entry)=>_.join(currPath, '|').startsWith(_.join(entry.source, '|')));
if(allListeners) {
for(const listener of allListeners) {
if(listener.defCallback) {
let thePromise = this._getDefListenerPromise(state, listener, actionObj);
if(thePromise) {
deferredList.push({
action: actionObj,
promise: thePromise,
});
}
}
}
}
return deferredList;
}
}

View File

@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////
import React, { useEffect, useMemo, useRef, useState } from 'react';
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { Box, makeStyles, Tab, Tabs } from '@material-ui/core';
import _ from 'lodash';
import PropTypes from 'prop-types';
@ -21,6 +21,8 @@ import { InputSQL } from '../components/FormComponents';
import gettext from 'sources/gettext';
import { evalFunc } from 'sources/utils';
import CustomPropTypes from '../custom_prop_types';
import { useOnScreen } from '../custom_hooks';
import { DepListenerContext } from './DepListener';
const useStyles = makeStyles((theme)=>({
fullSpace: {
@ -33,6 +35,9 @@ const useStyles = makeStyles((theme)=>({
nestedTabPanel: {
backgroundColor: theme.otherVars.headerBg,
},
nestedControl: {
height: 'unset',
}
}));
/* Optional SQL tab */
@ -67,28 +72,52 @@ SQLTab.propTypes = {
/* The first component of schema view form */
export default function FormView({
value, formErr, schema={}, viewHelperProps, isNested=false, accessPath, dataDispatch, hasSQLTab, getSQLValue, onTabChange, firstEleRef, className}) {
value, formErr, schema={}, viewHelperProps, isNested=false, accessPath, dataDispatch, hasSQLTab,
getSQLValue, onTabChange, firstEleRef, className, isDataGridForm=false}) {
let defaultTab = 'General';
let tabs = {};
let tabsClassname = {};
const [tabValue, setTabValue] = useState(0);
const classes = useStyles();
const firstElement = useRef();
const formRef = useRef();
const onScreenTracker = useRef(false);
const depListener = useContext(DepListenerContext);
let groupLabels = {};
schema = schema || {fields: []};
/* Calculate the fields which depends on the current field
deps has info on fields which the current field depends on. */
const dependsOnField = useMemo(()=>{
let res = {};
schema.fields.forEach((field)=>{
(field.deps || []).forEach((dep)=>{
res[dep] = res[dep] || [];
res[dep].push(field.id);
let isOnScreen = useOnScreen(formRef);
if(isOnScreen) {
/* Don't do it when the form is alredy visible */
if(onScreenTracker.current == false) {
/* Re-select the tab. If form is hidden then sometimes it is not selected */
setTabValue(tabValue);
onScreenTracker.current = true;
}
} else {
onScreenTracker.current = false;
}
useEffect(()=>{
/* Calculate the fields which depends on the current field */
if(!isDataGridForm) {
schema.fields.forEach((field)=>{
/* Self change is also dep change */
if(field.depChange || field.deferredDepChange) {
depListener.addDepListener(accessPath.concat(field.id), accessPath.concat(field.id), field.depChange, field.deferredDepChange);
}
(evalFunc(null, field.deps) || []).forEach((dep)=>{
let source = accessPath.concat(dep);
if(_.isArray(dep)) {
source = dep;
}
if(field.depChange) {
depListener.addDepListener(source, accessPath.concat(field.id), field.depChange);
}
});
});
});
return res;
}
}, []);
/* Prepare the array of components based on the types */
@ -127,19 +156,36 @@ export default function FormView({
/* Lets choose the path based on type */
if(field.type === 'nested-tab') {
/* Pass on the top schema */
field.schema.top = schema.top;
if(isNested) {
field.schema.top = schema.top;
} else {
field.schema.top = schema;
}
tabs[group].push(
<FormView key={`nested${tabs[group].length}`} value={value} viewHelperProps={viewHelperProps} formErr={formErr}
schema={field.schema} accessPath={accessPath} dataDispatch={dataDispatch} isNested={true} />
schema={field.schema} accessPath={accessPath} dataDispatch={dataDispatch} isNested={true} {...field}/>
);
} else if(field.type === 'collection') {
/* Pass on the top schema */
field.schema.top = schema.top;
/* If its a collection, let data grid view handle it */
let depsMap = [value[field.id]];
/* Pass on the top schema */
if(isNested) {
field.schema.top = schema.top;
} else {
field.schema.top = schema;
}
/* Eval the params based on state */
let {canAdd, canEdit, canDelete, ..._field} = field;
canAdd = evalFunc(schema, canAdd, value);
canEdit = evalFunc(schema, canAdd, value);
canDelete = evalFunc(schema, canAdd, value);
tabs[group].push(
useMemo(()=><DataGridView key={field.id} value={value[field.id]} viewHelperProps={viewHelperProps} formErr={formErr}
schema={field.schema} accessPath={accessPath.concat(field.id)} dataDispatch={dataDispatch} containerClassName={classes.controlRow}
{...field}/>, [value[field.id]])
useMemo(()=><DataGridView key={_field.id} value={value[_field.id]} viewHelperProps={viewHelperProps} formErr={formErr}
schema={_field.schema} accessPath={accessPath.concat(_field.id)} dataDispatch={dataDispatch} containerClassName={classes.controlRow}
canAdd={canAdd} canEdit={canEdit} canDelete={canDelete} {..._field}/>, depsMap)
);
} else if(field.type === 'group') {
groupLabels[field.id] = field.label;
@ -171,21 +217,10 @@ export default function FormView({
{...field}
onChange={(value)=>{
/* Get the changes on dependent fields as well */
const depChange = (state)=>{
field.depChange && _.merge(state, field.depChange(state) || {});
(dependsOnField[field.id] || []).forEach((d)=>{
d = _.find(schema.fields, (f)=>f.id==d);
if(d.depChange) {
_.merge(state, d.depChange(state) || {});
}
});
return state;
};
dataDispatch({
type: SCHEMA_STATE_ACTIONS.SET_VALUE,
path: accessPath.concat(field.id),
value: value,
depChange: depChange,
});
}}
hasError={hasError}
@ -197,7 +232,7 @@ export default function FormView({
_visible,
hasError,
classes.controlRow,
...(field.deps || []).map((dep)=>value[dep])
...(evalFunc(null, field.deps) || []).map((dep)=>value[dep]),
])
);
}
@ -206,8 +241,8 @@ export default function FormView({
/* Add the SQL tab if required */
let sqlTabActive = false;
let sqlTabName = gettext('SQL');
if(hasSQLTab) {
let sqlTabName = gettext('SQL');
sqlTabActive = (Object.keys(tabs).length === tabValue);
/* Re-render and fetch the SQL tab when it is active */
tabs[sqlTabName] = [
@ -226,7 +261,7 @@ export default function FormView({
return (
<>
<Box height="100%" display="flex" flexDirection="column" className={className}>
<Box height="100%" display="flex" flexDirection="column" className={className} ref={formRef}>
<Box>
<Tabs
value={tabValue}
@ -245,7 +280,8 @@ export default function FormView({
</Box>
{Object.keys(tabs).map((tabName, i)=>{
return (
<TabPanel key={tabName} value={tabValue} index={i} classNameRoot={clsx(tabsClassname[tabName], isNested ? classes.nestedTabPanel : null)}>
<TabPanel key={tabName} value={tabValue} index={i} classNameRoot={clsx(tabsClassname[tabName], isNested ? classes.nestedTabPanel : null)}
className={tabName != sqlTabName ? classes.nestedControl : null}>
{tabs[tabName]}
</TabPanel>
);

View File

@ -7,6 +7,8 @@
//
//////////////////////////////////////////////////////////////
import _ from "lodash";
/* This is the base schema class for SchemaView.
* A UI schema must inherit this to use SchemaView for UI.
*/
@ -28,8 +30,7 @@ export default class BaseUISchema {
}
get top() {
/* If no top, I'm the top */
return this._top || this;
return this._top;
}
/* The original data before any changes */
@ -41,6 +42,16 @@ export default class BaseUISchema {
return this._origData || {};
}
/* The session data, can be useful but setting this will not affect UI
this._sessData is set by SchemaView directly. set sessData should not be allowed anywhere */
get sessData() {
return this._sessData || {};
}
set sessData(val) {
throw new Error('Property sessData is readonly.', val);
}
/* Property allows to restrict setting this later */
get defaults() {
return this._defaults || {};
@ -102,4 +113,17 @@ export default class BaseUISchema {
validate() {
return false;
}
/* Returns the new data row for the schema based on defaults and input */
getNewData(data={}) {
let newRow = {};
this.fields.forEach((field)=>{
if(!_.isUndefined(data[field.id])){
newRow[field.id] = data[field.id];
} else {
newRow[field.id] = this.defaults[field.id];
}
});
return newRow;
}
}

View File

@ -34,6 +34,7 @@ import { evalFunc } from 'sources/utils';
import PropTypes from 'prop-types';
import CustomPropTypes from '../custom_prop_types';
import { parseApiError } from '../api_instance';
import DepListener, {DepListenerContext} from './DepListener';
const useDialogStyles = makeStyles((theme)=>({
root: {
@ -221,8 +222,36 @@ export const SCHEMA_STATE_ACTIONS = {
ADD_ROW: 'add_row',
DELETE_ROW: 'delete_row',
RERENDER: 'rerender',
CLEAR_DEFERRED_QUEUE: 'clear_deferred_queue',
DEFERRED_DEPCHANGE: 'deferred_depchange',
};
const getDepChange = (currPath, newState, oldState, action)=>{
if(action.depChange) {
newState = action.depChange(currPath, newState, {
type: action.type,
path: action.path,
value: action.value,
oldState: _.cloneDeep(oldState),
depChangeResolved: action.depChangeResolved,
});
}
return newState;
}
const getDeferredDepChange = (currPath, newState, oldState, action)=>{
if(action.deferredDepChange) {
let deferredPromiseList = action.deferredDepChange(currPath, newState, {
type: action.type,
path: action.path,
value: action.value,
depChange: action.depChange,
oldState: _.cloneDeep(oldState),
});
return deferredPromiseList
}
}
/* The main function which manipulates the session state based on actions */
/*
The state is managed based on path array of a particular key
@ -243,6 +272,7 @@ The state starts with path []
const sessDataReducer = (state, action)=>{
let data = _.cloneDeep(state);
let rows, cid;
data.__deferred__ = data.__deferred__ || [];
switch(action.type) {
case SCHEMA_STATE_ACTIONS.INIT:
data = action.payload;
@ -250,9 +280,10 @@ const sessDataReducer = (state, action)=>{
case SCHEMA_STATE_ACTIONS.SET_VALUE:
_.set(data, action.path, action.value);
/* If there is any dep listeners get the changes */
if(action.depChange) {
data = action.depChange(data);
}
data = getDepChange(action.path, data, state, action);
let deferredList = getDeferredDepChange(action.path, data, state, action);
// let deferredInfo = getDeferredDepChange(action.path, data, state, action);
data.__deferred__ = deferredList || [];
break;
case SCHEMA_STATE_ACTIONS.ADD_ROW:
/* Create id to identify a row uniquely, usefull when getting diff */
@ -260,11 +291,21 @@ const sessDataReducer = (state, action)=>{
action.value['cid'] = cid;
rows = (_.get(data, action.path)||[]).concat(action.value);
_.set(data, action.path, rows);
/* If there is any dep listeners get the changes */
data = getDepChange(action.path, data, state, action);
break;
case SCHEMA_STATE_ACTIONS.DELETE_ROW:
rows = _.get(data, action.path)||[];
rows.splice(action.value, 1);
_.set(data, action.path, rows);
/* If there is any dep listeners get the changes */
data = getDepChange(action.path, data, state, action);
break;
case SCHEMA_STATE_ACTIONS.CLEAR_DEFERRED_QUEUE:
data.__deferred__ = [];
break;
case SCHEMA_STATE_ACTIONS.DEFERRED_DEPCHANGE:
data = getDepChange(action.path, data, state, action);
break;
}
return data;
@ -317,12 +358,16 @@ function SchemaDialogView({
const [formReady, setFormReady] = useState(false);
const firstEleRef = useRef();
const isNew = schema.isNew(schema.origData);
const depListenerObj = useRef(new DepListener());
/* The session data */
const [sessData, sessDispatch] = useReducer(sessDataReducer, {});
useEffect(()=>{
/* if sessData changes, validate the schema */
if(!formReady) return;
/* Set the _sessData, can be usefull to some deep controls */
schema._sessData = sessData;
let isNotValid = validateSchema(schema, sessData, (name, message)=>{
if(message) {
setFormErr({
@ -341,6 +386,25 @@ function SchemaDialogView({
props.onDataChange && props.onDataChange(dataChanged);
}, [sessData]);
useEffect(()=>{
if(sessData.__deferred__?.length > 0) {
sessDispatch({
type: SCHEMA_STATE_ACTIONS.CLEAR_DEFERRED_QUEUE,
});
// let deferredDepChang = sessData.__deferred__[0];
let item = sessData.__deferred__[0];
item.promise.then((resFunc)=>{
sessDispatch({
type: SCHEMA_STATE_ACTIONS.DEFERRED_DEPCHANGE,
path: item.action.path,
depChange: item.action.depChange,
depChangeResolved: resFunc,
});
});
}
}, [sessData.__deferred__?.length]);
useEffect(()=>{
/* Docker on load focusses itself, so our focus should execute later */
let focusTimeout = setTimeout(()=>{
@ -470,37 +534,47 @@ function SchemaDialogView({
}
};
const sessDispatchWithListener = (action)=>{
sessDispatch({
...action,
depChange: (...args)=>depListenerObj.current.getDepChange(...args),
deferredDepChange: (...args)=>depListenerObj.current.getDeferredDepChange(...args),
});
};
/* I am Groot */
return (
<Box className={classes.root}>
<Box className={classes.form}>
<Loader message={loaderText}/>
<FormView value={sessData} viewHelperProps={viewHelperProps} formErr={formErr}
schema={schema} accessPath={[]} dataDispatch={sessDispatch}
hasSQLTab={props.hasSQL} getSQLValue={getSQLValue} onTabChange={(i, tabName, sqlActive)=>setSqlTabActive(sqlActive)}
firstEleRef={firstEleRef} />
<FormFooterMessage type={MESSAGE_TYPE.ERROR} message={sqlTabActive ? '' : formErr.message}
onClose={onErrClose} />
</Box>
<Box className={classes.footer}>
{useMemo(()=><Box>
<PgIconButton data-test="sql-help" onClick={()=>props.onHelp(true, isNew)} icon={<InfoIcon />}
disabled={props.disableSqlHelp} className={classes.buttonMargin} title="SQL help for this object type."/>
<PgIconButton data-test="dialog-help" onClick={()=>props.onHelp(false, isNew)} icon={<HelpIcon />} title="Help for this dialog."/>
</Box>, [])}
<Box marginLeft="auto">
<DefaultButton data-test="Close" onClick={props.onClose} startIcon={<CloseIcon />} className={classes.buttonMargin}>
{gettext('Close')}
</DefaultButton>
<DefaultButton data-test="Reset" onClick={onResetClick} startIcon={<SettingsBackupRestoreIcon />} disabled={!dirty || saving} className={classes.buttonMargin}>
{gettext('Reset')}
</DefaultButton>
<PrimaryButton data-test="Save" onClick={onSaveClick} startIcon={<SaveIcon />} disabled={!dirty || saving || Boolean(formErr.name) || !formReady}>
{gettext('Save')}
</PrimaryButton>
<DepListenerContext.Provider value={depListenerObj.current}>
<Box className={classes.root}>
<Box className={classes.form}>
<Loader message={loaderText}/>
<FormView value={sessData} viewHelperProps={viewHelperProps} formErr={formErr}
schema={schema} accessPath={[]} dataDispatch={sessDispatchWithListener}
hasSQLTab={props.hasSQL} getSQLValue={getSQLValue} onTabChange={(i, tabName, sqlActive)=>setSqlTabActive(sqlActive)}
firstEleRef={firstEleRef} />
<FormFooterMessage type={MESSAGE_TYPE.ERROR} message={formErr.message}
onClose={onErrClose} />
</Box>
<Box className={classes.footer}>
{useMemo(()=><Box>
<PgIconButton data-test="sql-help" onClick={()=>props.onHelp(true, isNew)} icon={<InfoIcon />}
disabled={props.disableSqlHelp} className={classes.buttonMargin} title="SQL help for this object type."/>
<PgIconButton data-test="dialog-help" onClick={()=>props.onHelp(false, isNew)} icon={<HelpIcon />} title="Help for this dialog."/>
</Box>, [])}
<Box marginLeft="auto">
<DefaultButton data-test="Close" onClick={props.onClose} startIcon={<CloseIcon />} className={classes.buttonMargin}>
{gettext('Close')}
</DefaultButton>
<DefaultButton data-test="Reset" onClick={onResetClick} startIcon={<SettingsBackupRestoreIcon />} disabled={!dirty || saving} className={classes.buttonMargin}>
{gettext('Reset')}
</DefaultButton>
<PrimaryButton data-test="Save" onClick={onSaveClick} startIcon={<SaveIcon />} disabled={!dirty || saving || Boolean(formErr.name) || !formReady}>
{gettext('Save')}
</PrimaryButton>
</Box>
</Box>
</Box>
</Box>
</DepListenerContext.Provider>
);
}

View File

@ -19,6 +19,9 @@ const useStyles = makeStyles((theme)=>({
padding: theme.spacing(1),
overflow: 'auto',
backgroundColor: theme.palette.grey[400]
},
content: {
height: '100%',
}
}));
@ -28,7 +31,7 @@ export default function TabPanel({children, classNameRoot, className, value, ind
const active = value === index;
return (
<Box className={clsx(classes.root, classNameRoot)} component="div" hidden={!active}>
<Box style={{height: '100%'}} className={className}>{children}</Box>
<Box className={clsx(classes.content, className)}>{children}</Box>
</Box>
);
}

View File

@ -47,14 +47,14 @@ export function integerValidator(label, value) {
/* Validate value to check if it is empty */
export function emptyValidator(label, value) {
if(isEmptyString(value) || String(value).replace(/^\s+|\s+$/g, '') == '') {
if(isEmptyString(value)) {
return sprintf(pgAdmin.Browser.messages.CANNOT_BE_EMPTY, label);
}
return null;
}
export function isEmptyString(string) {
return _.isUndefined(string) || _.isNull(string) || String(string).trim() === '';
export function isEmptyString(value) {
return _.isUndefined(value) || _.isNull(value) || String(value).trim() === '' || String(value).replace(/^\s+|\s+$/g, '') == '';
}
/* Validate rows to check for any duplicate rows based on uniqueCols-columns array */