mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-01-09 23:54:09 -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 */
|
||||
|
||||
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>;
|
||||
})}
|
||||
|
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 _ 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>
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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 */
|
||||
|
Loading…
Reference in New Issue
Block a user