mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-01-10 08:04:36 -06:00
- 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:
parent
5c1ce23780
commit
a06f78b2d5
@ -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>;
|
||||||
})}
|
})}
|
||||||
|
77
web/pgadmin/static/js/SchemaView/DepListener.js
Normal file
77
web/pgadmin/static/js/SchemaView/DepListener.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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 */
|
||||||
|
Loading…
Reference in New Issue
Block a user