- 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 */ /* 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 { Box } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import { PgIconButton } from '../components/Buttons'; import { PgIconButton } from '../components/Buttons';
@ -28,6 +28,9 @@ import FormView from './FormView';
import { confirmDeleteRow } from '../helpers/legacyConnector'; import { confirmDeleteRow } from '../helpers/legacyConnector';
import CustomPropTypes from 'sources/custom_prop_types'; import CustomPropTypes from 'sources/custom_prop_types';
import { evalFunc } from 'sources/utils'; import { evalFunc } from 'sources/utils';
import { useOnScreen } from '../custom_hooks';
import { DepListenerContext } from './DepListener';
import { getSchemaRow } from '../utils';
const useStyles = makeStyles((theme)=>({ const useStyles = makeStyles((theme)=>({
grid: { grid: {
@ -57,6 +60,9 @@ const useStyles = makeStyles((theme)=>({
padding: 0, padding: 0,
minWidth: 0, minWidth: 0,
backgroundColor: 'inherit', backgroundColor: 'inherit',
'&.Mui-disabled': {
border: 0,
},
}, },
gridTableContainer: { gridTableContainer: {
overflow: 'auto', overflow: 'auto',
@ -138,13 +144,33 @@ DataTableHeader.propTypes = {
headerGroups: PropTypes.array.isRequired, headerGroups: PropTypes.array.isRequired,
}; };
function DataTableRow({row, totalRows, isResizing}) { function DataTableRow({row, totalRows, isResizing, schema, accessPath}) {
const classes = useStyles(); const classes = useStyles();
const [key, setKey] = useState(false); const [key, setKey] = useState(false);
const depListener = useContext(DepListenerContext);
/* Memoize the row to avoid unnecessary re-render. /* Memoize the row to avoid unnecessary re-render.
* If table data changes, then react-table re-renders the complete tables * If table data changes, then react-table re-renders the complete tables
* We can avoid re-render by if row data is not changed * 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'))); let depsMap = _.values(row.values, Object.keys(row.values).filter((k)=>!k.startsWith('btn')));
depsMap = depsMap.concat([totalRows, row.isExpanded, key, isResizing]); depsMap = depsMap.concat([totalRows, row.isExpanded, key, isResizing]);
return useMemo(()=> return useMemo(()=>
@ -168,18 +194,7 @@ function DataTableRow({row, totalRows, isResizing}) {
export default function DataGridView({ export default function DataGridView({
value, viewHelperProps, formErr, schema, accessPath, dataDispatch, containerClassName, ...props}) { value, viewHelperProps, formErr, schema, accessPath, dataDispatch, containerClassName, ...props}) {
const classes = useStyles(); 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 */ /* Using ref so that schema variable is not frozen in columns closure */
const schemaRef = useRef(schema); const schemaRef = useRef(schema);
let columns = useMemo( let columns = useMemo(
@ -195,11 +210,17 @@ export default function DataGridView({
dataType: 'edit', dataType: 'edit',
width: 30, width: 30,
minWidth: '0', minWidth: '0',
Cell: ({row})=><PgIconButton data-test="expand-row" title={gettext('Edit row')} icon={<EditRoundedIcon />} className={classes.gridRowButton} Cell: ({row})=>{
onClick={()=>{ let canEditRow = true;
row.toggleRowExpanded(!row.isExpanded); 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.displayName = 'Cell',
colInfo.Cell.propTypes = { colInfo.Cell.propTypes = {
@ -218,17 +239,23 @@ export default function DataGridView({
width: 30, width: 30,
minWidth: '0', minWidth: '0',
Cell: ({row}) => { Cell: ({row}) => {
let canDeleteRow = true;
if(props.canDeleteRow) {
canDeleteRow = evalFunc(schemaRef.current, props.canDeleteRow, row.original || {});
}
return ( return (
<PgIconButton data-test="delete-row" title={gettext('Delete row')} icon={<DeleteRoundedIcon />} <PgIconButton data-test="delete-row" title={gettext('Delete row')} icon={<DeleteRoundedIcon />}
onClick={()=>{ onClick={()=>{
confirmDeleteRow(()=>{ confirmDeleteRow(()=>{
/* Get the changes on dependent fields as well */
dataDispatch({ dataDispatch({
type: SCHEMA_STATE_ACTIONS.DELETE_ROW, type: SCHEMA_STATE_ACTIONS.DELETE_ROW,
path: accessPath, path: accessPath,
value: row.index, value: row.index,
}); });
}, ()=>{}, props.customDeleteTitle, props.customDeleteMsg); }, ()=>{}, props.customDeleteTitle, props.customDeleteMsg);
}} className={classes.gridRowButton} /> }} className={classes.gridRowButton} disabled={!canDeleteRow} />
); );
} }
}; };
@ -287,25 +314,10 @@ export default function DataGridView({
disabled={!editable} disabled={!editable}
visible={_visible} visible={_visible}
onCellChange={(value)=>{ 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({ dataDispatch({
type: SCHEMA_STATE_ACTIONS.SET_VALUE, type: SCHEMA_STATE_ACTIONS.SET_VALUE,
path: accessPath.concat([row.index, _field.id]), path: accessPath.concat([row.index, _field.id]),
value: value, value: value,
depChange: depChange,
}); });
}} }}
reRenderRow={other.reRenderRow} reRenderRow={other.reRenderRow}
@ -326,12 +338,7 @@ export default function DataGridView({
); );
const onAddClick = useCallback(()=>{ const onAddClick = useCallback(()=>{
let newRow = {}; let newRow = schemaRef.current.getNewData();
columns.forEach((column)=>{
if(column.field) {
newRow[column.field.id] = schemaRef.current.defaults[column.field.id];
}
});
dataDispatch({ dataDispatch({
type: SCHEMA_STATE_ACTIONS.ADD_ROW, type: SCHEMA_STATE_ACTIONS.ADD_ROW,
path: accessPath, path: accessPath,
@ -387,12 +394,12 @@ export default function DataGridView({
{rows.map((row, i) => { {rows.map((row, i) => {
prepareRow(row); prepareRow(row);
return <React.Fragment key={i}> return <React.Fragment key={i}>
<DataTableRow row={row} totalRows={rows.length} canExpand={props.canEdit} <DataTableRow row={row} totalRows={rows.length} isResizing={isResizing}
value={value} viewHelperProps={viewHelperProps} formErr={formErr} isResizing={isResizing} schema={schemaRef.current} accessPath={accessPath.concat([row.index])} />
schema={schemaRef.current} accessPath={accessPath.concat([row.index])} dataDispatch={dataDispatch} />
{props.canEdit && row.isExpanded && {props.canEdit && row.isExpanded &&
<FormView value={row.original} viewHelperProps={viewHelperProps} formErr={formErr} dataDispatch={dataDispatch} <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>; </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 { Box, makeStyles, Tab, Tabs } from '@material-ui/core';
import _ from 'lodash'; import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -21,6 +21,8 @@ import { InputSQL } from '../components/FormComponents';
import gettext from 'sources/gettext'; import gettext from 'sources/gettext';
import { evalFunc } from 'sources/utils'; import { evalFunc } from 'sources/utils';
import CustomPropTypes from '../custom_prop_types'; import CustomPropTypes from '../custom_prop_types';
import { useOnScreen } from '../custom_hooks';
import { DepListenerContext } from './DepListener';
const useStyles = makeStyles((theme)=>({ const useStyles = makeStyles((theme)=>({
fullSpace: { fullSpace: {
@ -33,6 +35,9 @@ const useStyles = makeStyles((theme)=>({
nestedTabPanel: { nestedTabPanel: {
backgroundColor: theme.otherVars.headerBg, backgroundColor: theme.otherVars.headerBg,
}, },
nestedControl: {
height: 'unset',
}
})); }));
/* Optional SQL tab */ /* Optional SQL tab */
@ -67,28 +72,52 @@ SQLTab.propTypes = {
/* The first component of schema view form */ /* The first component of schema view form */
export default function FormView({ 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 defaultTab = 'General';
let tabs = {}; let tabs = {};
let tabsClassname = {}; let tabsClassname = {};
const [tabValue, setTabValue] = useState(0); const [tabValue, setTabValue] = useState(0);
const classes = useStyles(); const classes = useStyles();
const firstElement = useRef(); const firstElement = useRef();
const formRef = useRef();
const onScreenTracker = useRef(false);
const depListener = useContext(DepListenerContext);
let groupLabels = {}; let groupLabels = {};
schema = schema || {fields: []}; schema = schema || {fields: []};
/* Calculate the fields which depends on the current field let isOnScreen = useOnScreen(formRef);
deps has info on fields which the current field depends on. */ if(isOnScreen) {
const dependsOnField = useMemo(()=>{ /* Don't do it when the form is alredy visible */
let res = {}; if(onScreenTracker.current == false) {
schema.fields.forEach((field)=>{ /* Re-select the tab. If form is hidden then sometimes it is not selected */
(field.deps || []).forEach((dep)=>{ setTabValue(tabValue);
res[dep] = res[dep] || []; onScreenTracker.current = true;
res[dep].push(field.id); }
} 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 */ /* Prepare the array of components based on the types */
@ -127,19 +156,36 @@ export default function FormView({
/* Lets choose the path based on type */ /* Lets choose the path based on type */
if(field.type === 'nested-tab') { if(field.type === 'nested-tab') {
/* Pass on the top schema */ /* 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( tabs[group].push(
<FormView key={`nested${tabs[group].length}`} value={value} viewHelperProps={viewHelperProps} formErr={formErr} <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') { } 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 */ /* 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( tabs[group].push(
useMemo(()=><DataGridView key={field.id} value={value[field.id]} viewHelperProps={viewHelperProps} formErr={formErr} 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} schema={_field.schema} accessPath={accessPath.concat(_field.id)} dataDispatch={dataDispatch} containerClassName={classes.controlRow}
{...field}/>, [value[field.id]]) canAdd={canAdd} canEdit={canEdit} canDelete={canDelete} {..._field}/>, depsMap)
); );
} else if(field.type === 'group') { } else if(field.type === 'group') {
groupLabels[field.id] = field.label; groupLabels[field.id] = field.label;
@ -171,21 +217,10 @@ export default function FormView({
{...field} {...field}
onChange={(value)=>{ onChange={(value)=>{
/* Get the changes on dependent fields as well */ /* 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({ dataDispatch({
type: SCHEMA_STATE_ACTIONS.SET_VALUE, type: SCHEMA_STATE_ACTIONS.SET_VALUE,
path: accessPath.concat(field.id), path: accessPath.concat(field.id),
value: value, value: value,
depChange: depChange,
}); });
}} }}
hasError={hasError} hasError={hasError}
@ -197,7 +232,7 @@ export default function FormView({
_visible, _visible,
hasError, hasError,
classes.controlRow, 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 */ /* Add the SQL tab if required */
let sqlTabActive = false; let sqlTabActive = false;
let sqlTabName = gettext('SQL');
if(hasSQLTab) { if(hasSQLTab) {
let sqlTabName = gettext('SQL');
sqlTabActive = (Object.keys(tabs).length === tabValue); sqlTabActive = (Object.keys(tabs).length === tabValue);
/* Re-render and fetch the SQL tab when it is active */ /* Re-render and fetch the SQL tab when it is active */
tabs[sqlTabName] = [ tabs[sqlTabName] = [
@ -226,7 +261,7 @@ export default function FormView({
return ( return (
<> <>
<Box height="100%" display="flex" flexDirection="column" className={className}> <Box height="100%" display="flex" flexDirection="column" className={className} ref={formRef}>
<Box> <Box>
<Tabs <Tabs
value={tabValue} value={tabValue}
@ -245,7 +280,8 @@ export default function FormView({
</Box> </Box>
{Object.keys(tabs).map((tabName, i)=>{ {Object.keys(tabs).map((tabName, i)=>{
return ( 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]} {tabs[tabName]}
</TabPanel> </TabPanel>
); );

View File

@ -7,6 +7,8 @@
// //
////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////
import _ from "lodash";
/* This is the base schema class for SchemaView. /* This is the base schema class for SchemaView.
* A UI schema must inherit this to use SchemaView for UI. * A UI schema must inherit this to use SchemaView for UI.
*/ */
@ -28,8 +30,7 @@ export default class BaseUISchema {
} }
get top() { get top() {
/* If no top, I'm the top */ return this._top;
return this._top || this;
} }
/* The original data before any changes */ /* The original data before any changes */
@ -41,6 +42,16 @@ export default class BaseUISchema {
return this._origData || {}; 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 */ /* Property allows to restrict setting this later */
get defaults() { get defaults() {
return this._defaults || {}; return this._defaults || {};
@ -102,4 +113,17 @@ export default class BaseUISchema {
validate() { validate() {
return false; 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 PropTypes from 'prop-types';
import CustomPropTypes from '../custom_prop_types'; import CustomPropTypes from '../custom_prop_types';
import { parseApiError } from '../api_instance'; import { parseApiError } from '../api_instance';
import DepListener, {DepListenerContext} from './DepListener';
const useDialogStyles = makeStyles((theme)=>({ const useDialogStyles = makeStyles((theme)=>({
root: { root: {
@ -221,8 +222,36 @@ export const SCHEMA_STATE_ACTIONS = {
ADD_ROW: 'add_row', ADD_ROW: 'add_row',
DELETE_ROW: 'delete_row', DELETE_ROW: 'delete_row',
RERENDER: 'rerender', 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 main function which manipulates the session state based on actions */
/* /*
The state is managed based on path array of a particular key 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)=>{ const sessDataReducer = (state, action)=>{
let data = _.cloneDeep(state); let data = _.cloneDeep(state);
let rows, cid; let rows, cid;
data.__deferred__ = data.__deferred__ || [];
switch(action.type) { switch(action.type) {
case SCHEMA_STATE_ACTIONS.INIT: case SCHEMA_STATE_ACTIONS.INIT:
data = action.payload; data = action.payload;
@ -250,9 +280,10 @@ const sessDataReducer = (state, action)=>{
case SCHEMA_STATE_ACTIONS.SET_VALUE: case SCHEMA_STATE_ACTIONS.SET_VALUE:
_.set(data, action.path, action.value); _.set(data, action.path, action.value);
/* If there is any dep listeners get the changes */ /* If there is any dep listeners get the changes */
if(action.depChange) { data = getDepChange(action.path, data, state, action);
data = action.depChange(data); let deferredList = getDeferredDepChange(action.path, data, state, action);
} // let deferredInfo = getDeferredDepChange(action.path, data, state, action);
data.__deferred__ = deferredList || [];
break; break;
case SCHEMA_STATE_ACTIONS.ADD_ROW: case SCHEMA_STATE_ACTIONS.ADD_ROW:
/* Create id to identify a row uniquely, usefull when getting diff */ /* Create id to identify a row uniquely, usefull when getting diff */
@ -260,11 +291,21 @@ const sessDataReducer = (state, action)=>{
action.value['cid'] = cid; action.value['cid'] = cid;
rows = (_.get(data, action.path)||[]).concat(action.value); rows = (_.get(data, action.path)||[]).concat(action.value);
_.set(data, action.path, rows); _.set(data, action.path, rows);
/* If there is any dep listeners get the changes */
data = getDepChange(action.path, data, state, action);
break; break;
case SCHEMA_STATE_ACTIONS.DELETE_ROW: case SCHEMA_STATE_ACTIONS.DELETE_ROW:
rows = _.get(data, action.path)||[]; rows = _.get(data, action.path)||[];
rows.splice(action.value, 1); rows.splice(action.value, 1);
_.set(data, action.path, rows); _.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; break;
} }
return data; return data;
@ -317,12 +358,16 @@ function SchemaDialogView({
const [formReady, setFormReady] = useState(false); const [formReady, setFormReady] = useState(false);
const firstEleRef = useRef(); const firstEleRef = useRef();
const isNew = schema.isNew(schema.origData); const isNew = schema.isNew(schema.origData);
const depListenerObj = useRef(new DepListener());
/* The session data */ /* The session data */
const [sessData, sessDispatch] = useReducer(sessDataReducer, {}); const [sessData, sessDispatch] = useReducer(sessDataReducer, {});
useEffect(()=>{ useEffect(()=>{
/* if sessData changes, validate the schema */ /* if sessData changes, validate the schema */
if(!formReady) return; if(!formReady) return;
/* Set the _sessData, can be usefull to some deep controls */
schema._sessData = sessData;
let isNotValid = validateSchema(schema, sessData, (name, message)=>{ let isNotValid = validateSchema(schema, sessData, (name, message)=>{
if(message) { if(message) {
setFormErr({ setFormErr({
@ -341,6 +386,25 @@ function SchemaDialogView({
props.onDataChange && props.onDataChange(dataChanged); props.onDataChange && props.onDataChange(dataChanged);
}, [sessData]); }, [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(()=>{ useEffect(()=>{
/* Docker on load focusses itself, so our focus should execute later */ /* Docker on load focusses itself, so our focus should execute later */
let focusTimeout = setTimeout(()=>{ 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 */ /* I am Groot */
return ( return (
<Box className={classes.root}> <DepListenerContext.Provider value={depListenerObj.current}>
<Box className={classes.form}> <Box className={classes.root}>
<Loader message={loaderText}/> <Box className={classes.form}>
<FormView value={sessData} viewHelperProps={viewHelperProps} formErr={formErr} <Loader message={loaderText}/>
schema={schema} accessPath={[]} dataDispatch={sessDispatch} <FormView value={sessData} viewHelperProps={viewHelperProps} formErr={formErr}
hasSQLTab={props.hasSQL} getSQLValue={getSQLValue} onTabChange={(i, tabName, sqlActive)=>setSqlTabActive(sqlActive)} schema={schema} accessPath={[]} dataDispatch={sessDispatchWithListener}
firstEleRef={firstEleRef} /> hasSQLTab={props.hasSQL} getSQLValue={getSQLValue} onTabChange={(i, tabName, sqlActive)=>setSqlTabActive(sqlActive)}
<FormFooterMessage type={MESSAGE_TYPE.ERROR} message={sqlTabActive ? '' : formErr.message} firstEleRef={firstEleRef} />
onClose={onErrClose} /> <FormFooterMessage type={MESSAGE_TYPE.ERROR} message={formErr.message}
</Box> onClose={onErrClose} />
<Box className={classes.footer}> </Box>
{useMemo(()=><Box> <Box className={classes.footer}>
<PgIconButton data-test="sql-help" onClick={()=>props.onHelp(true, isNew)} icon={<InfoIcon />} {useMemo(()=><Box>
disabled={props.disableSqlHelp} className={classes.buttonMargin} title="SQL help for this object type."/> <PgIconButton data-test="sql-help" onClick={()=>props.onHelp(true, isNew)} icon={<InfoIcon />}
<PgIconButton data-test="dialog-help" onClick={()=>props.onHelp(false, isNew)} icon={<HelpIcon />} title="Help for this dialog."/> disabled={props.disableSqlHelp} className={classes.buttonMargin} title="SQL help for this object type."/>
</Box>, [])} <PgIconButton data-test="dialog-help" onClick={()=>props.onHelp(false, isNew)} icon={<HelpIcon />} title="Help for this dialog."/>
<Box marginLeft="auto"> </Box>, [])}
<DefaultButton data-test="Close" onClick={props.onClose} startIcon={<CloseIcon />} className={classes.buttonMargin}> <Box marginLeft="auto">
{gettext('Close')} <DefaultButton data-test="Close" onClick={props.onClose} startIcon={<CloseIcon />} className={classes.buttonMargin}>
</DefaultButton> {gettext('Close')}
<DefaultButton data-test="Reset" onClick={onResetClick} startIcon={<SettingsBackupRestoreIcon />} disabled={!dirty || saving} className={classes.buttonMargin}> </DefaultButton>
{gettext('Reset')} <DefaultButton data-test="Reset" onClick={onResetClick} startIcon={<SettingsBackupRestoreIcon />} disabled={!dirty || saving} className={classes.buttonMargin}>
</DefaultButton> {gettext('Reset')}
<PrimaryButton data-test="Save" onClick={onSaveClick} startIcon={<SaveIcon />} disabled={!dirty || saving || Boolean(formErr.name) || !formReady}> </DefaultButton>
{gettext('Save')} <PrimaryButton data-test="Save" onClick={onSaveClick} startIcon={<SaveIcon />} disabled={!dirty || saving || Boolean(formErr.name) || !formReady}>
</PrimaryButton> {gettext('Save')}
</PrimaryButton>
</Box>
</Box> </Box>
</Box> </Box>
</Box> </DepListenerContext.Provider>
); );
} }

View File

@ -19,6 +19,9 @@ const useStyles = makeStyles((theme)=>({
padding: theme.spacing(1), padding: theme.spacing(1),
overflow: 'auto', overflow: 'auto',
backgroundColor: theme.palette.grey[400] 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; const active = value === index;
return ( return (
<Box className={clsx(classes.root, classNameRoot)} component="div" hidden={!active}> <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> </Box>
); );
} }

View File

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