Introduce custom React Hook useSchemaState to simplify SchemaView component. #7776
Changes include: - Simplify current SchemaView code - Add ability to reuse the schema data & state management implementation outside the SchemaDialogView component. - Further split components in small and manageable separate files. - Removed the 'DepListenerContext' context as there was no need for separate context. - Added a reload functionality in the 'useSchemaState' - Changes in feature tests.pull/7777/head
parent
546806c40c
commit
52af8d3e49
|
@ -172,7 +172,7 @@
|
|||
"pep8": "pycodestyle --config=../.pycodestyle ../docs && pycodestyle --config=../.pycodestyle ../pkg && pycodestyle --config=../.pycodestyle ../tools && pycodestyle --config=../.pycodestyle ../web",
|
||||
"auditjs-html": "yarn audit --json | yarn run yarn-audit-html --output ../auditjs.html",
|
||||
"auditjs": "yarn audit --groups dependencies",
|
||||
"auditpy": "safety check --full-report -i 51668 -i 52495yarn npm audit",
|
||||
"auditpy": "safety check --full-report -i 51668 -i 52495",
|
||||
"audit-all": "yarn run auditjs && yarn run auditpy"
|
||||
},
|
||||
"packageManager": "yarn@3.8.3",
|
||||
|
|
|
@ -42,7 +42,18 @@ export default function ObjectNodeProperties({panelId, node, treeNodeInfo, nodeD
|
|||
let warnOnCloseFlag = true;
|
||||
const confirmOnCloseReset = usePreferences().getPreferencesForModule('browser').confirm_on_properties_close;
|
||||
let updatedData = ['table', 'partition'].includes(nodeType) && !_.isEmpty(nodeData.rows_cnt) ? {rows_cnt: nodeData.rows_cnt} : undefined;
|
||||
let schema = node.getSchema(treeNodeInfo, nodeData);
|
||||
|
||||
const objToString = (obj) => (
|
||||
(obj && typeof obj === 'object') ? Object.keys(obj).sort().reduce(
|
||||
(acc, key) => (acc + `${key}=` + objToString(obj[key])), ''
|
||||
) : String(obj)
|
||||
);
|
||||
|
||||
const treeNodeId = objToString(treeNodeInfo);
|
||||
|
||||
let schema = useMemo(
|
||||
() => node.getSchema(treeNodeInfo, nodeData), [treeNodeId, isActive]
|
||||
);
|
||||
|
||||
// We only have two actionTypes, 'create' and 'edit' to initiate the dialog,
|
||||
// so if isActionTypeCopy is true, we should revert back to "create" since
|
||||
|
|
|
@ -491,7 +491,9 @@ export default function PreferencesComponent({ ...props }) {
|
|||
function savePreferences(data, initVal) {
|
||||
let _data = [];
|
||||
for (const [key, value] of Object.entries(data.current)) {
|
||||
let _metadata = prefSchema.current.schemaFields.filter((el) => { return el.id == key; });
|
||||
let _metadata = prefSchema.current.schemaFields.filter(
|
||||
(el) => { return el.id == key; }
|
||||
);
|
||||
if (_metadata.length > 0) {
|
||||
let val = getCollectionValue(_metadata, value, initVal);
|
||||
_data.push({
|
||||
|
@ -525,7 +527,11 @@ export default function PreferencesComponent({ ...props }) {
|
|||
data: save_data,
|
||||
}).then(() => {
|
||||
let requiresTreeRefresh = save_data.some((s)=>{
|
||||
return s.name=='show_system_objects'||s.name=='show_empty_coll_nodes'||s.name.startsWith('show_node_')||s.name=='hide_shared_server'||s.name=='show_user_defined_templates';
|
||||
return (
|
||||
s.name=='show_system_objects' || s.name=='show_empty_coll_nodes' ||
|
||||
s.name.startsWith('show_node_') || s.name=='hide_shared_server' ||
|
||||
s.name=='show_user_defined_templates'
|
||||
);
|
||||
});
|
||||
let requires_refresh = false;
|
||||
for (const [key] of Object.entries(data.current)) {
|
||||
|
@ -536,11 +542,15 @@ export default function PreferencesComponent({ ...props }) {
|
|||
if (requiresTreeRefresh) {
|
||||
pgAdmin.Browser.notifier.confirm(
|
||||
gettext('Object explorer refresh required'),
|
||||
gettext('An object explorer refresh is required. Do you wish to refresh it now?'),
|
||||
gettext(
|
||||
'An object explorer refresh is required. Do you wish to refresh it now?'
|
||||
),
|
||||
function () {
|
||||
pgAdmin.Browser.tree.destroy().then(
|
||||
() => {
|
||||
pgAdmin.Browser.Events.trigger('pgadmin-browser:tree:destroyed', undefined, undefined);
|
||||
pgAdmin.Browser.Events.trigger(
|
||||
'pgadmin-browser:tree:destroyed', undefined, undefined
|
||||
);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
|
|
@ -10,12 +10,8 @@
|
|||
/* The DataGridView component is based on react-table component */
|
||||
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { Box } from '@mui/material';
|
||||
import { PgIconButton } from '../components/Buttons';
|
||||
import AddIcon from '@mui/icons-material/AddOutlined';
|
||||
import { MappedCellControl } from './MappedControl';
|
||||
|
||||
import {
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
|
@ -24,103 +20,41 @@ import {
|
|||
getExpandedRowModel,
|
||||
flexRender,
|
||||
} from '@tanstack/react-table';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import PropTypes from 'prop-types';
|
||||
import _ from 'lodash';
|
||||
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
||||
import {HTML5Backend} from 'react-dnd-html5-backend';
|
||||
|
||||
import gettext from 'sources/gettext';
|
||||
import { SCHEMA_STATE_ACTIONS, StateUtilsContext } from '.';
|
||||
import FormView, { getFieldMetaData } from './FormView';
|
||||
import CustomPropTypes from 'sources/custom_prop_types';
|
||||
import { evalFunc } from 'sources/utils';
|
||||
import { DepListenerContext } from './DepListener';
|
||||
import { useIsMounted } from '../custom_hooks';
|
||||
import { InputText } from '../components/FormComponents';
|
||||
import { usePgAdmin } from '../BrowserComponent';
|
||||
import { requestAnimationAndFocus } from '../utils';
|
||||
import { PgReactTable, PgReactTableBody, PgReactTableCell, PgReactTableHeader,
|
||||
import { usePgAdmin } from 'sources/BrowserComponent';
|
||||
import { PgIconButton } from 'sources/components/Buttons';
|
||||
import {
|
||||
PgReactTable, PgReactTableBody, PgReactTableCell, PgReactTableHeader,
|
||||
PgReactTableRow, PgReactTableRowContent, PgReactTableRowExpandContent,
|
||||
getDeleteCell, getEditCell, getReorderCell } from '../components/PgReactTableStyled';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
getDeleteCell, getEditCell, getReorderCell
|
||||
} from 'sources/components/PgReactTableStyled';
|
||||
import CustomPropTypes from 'sources/custom_prop_types';
|
||||
import { useIsMounted } from 'sources/custom_hooks';
|
||||
import { InputText } from 'sources/components/FormComponents';
|
||||
import gettext from 'sources/gettext';
|
||||
import { evalFunc, requestAnimationAndFocus } from 'sources/utils';
|
||||
|
||||
const StyledBox = styled(Box)(({theme}) => ({
|
||||
'& .DataGridView-grid': {
|
||||
...theme.mixins.panelBorder,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: 0,
|
||||
height: '100%',
|
||||
'& .DataGridView-gridHeader': {
|
||||
display: 'flex',
|
||||
...theme.mixins.panelBorder.bottom,
|
||||
backgroundColor: theme.otherVars.headerBg,
|
||||
'& .DataGridView-gridHeaderText': {
|
||||
padding: theme.spacing(0.5, 1),
|
||||
fontWeight: theme.typography.fontWeightBold,
|
||||
},
|
||||
'& .DataGridView-gridControls': {
|
||||
marginLeft: 'auto',
|
||||
'& .DataGridView-gridControlsButton': {
|
||||
border: 0,
|
||||
borderRadius: 0,
|
||||
...theme.mixins.panelBorder.left,
|
||||
},
|
||||
},
|
||||
},
|
||||
'& .DataGridView-table': {
|
||||
'&.pgrt-table': {
|
||||
'& .pgrt-body':{
|
||||
'& .pgrt-row': {
|
||||
backgroundColor: theme.otherVars.emptySpaceBg,
|
||||
'& .pgrt-row-content':{
|
||||
'& .pgrd-row-cell': {
|
||||
height: 'auto',
|
||||
padding: theme.spacing(0.5),
|
||||
'&.btn-cell, &.expanded-icon-cell': {
|
||||
padding: '2px 0px'
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
'& .DataGridView-tableRowHovered': {
|
||||
position: 'relative',
|
||||
'& .hover-overlay': {
|
||||
backgroundColor: theme.palette.primary.light,
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
opacity: 0.75,
|
||||
}
|
||||
},
|
||||
'& .DataGridView-resizer': {
|
||||
display: 'inline-block',
|
||||
width: '5px',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
transform: 'translateX(50%)',
|
||||
zIndex: 1,
|
||||
touchAction: 'none',
|
||||
},
|
||||
'& .DataGridView-expandedForm': {
|
||||
border: '1px solid '+theme.palette.grey[400],
|
||||
},
|
||||
'& .DataGridView-expandedIconCell': {
|
||||
backgroundColor: theme.palette.grey[400],
|
||||
borderBottom: 'none',
|
||||
}
|
||||
}));
|
||||
import FormView from './FormView';
|
||||
import { MappedCellControl } from './MappedControl';
|
||||
import {
|
||||
SCHEMA_STATE_ACTIONS, SchemaStateContext, getFieldMetaData,
|
||||
isModeSupportedByField
|
||||
} from './common';
|
||||
import { StyleDataGridBox } from './StyledComponents';
|
||||
|
||||
function DataTableRow({index, row, totalRows, isResizing, isHovered, schema, schemaRef, accessPath, moveRow, setHoverIndex, viewHelperProps}) {
|
||||
|
||||
function DataTableRow({
|
||||
index, row, totalRows, isResizing, isHovered, schema, schemaRef, accessPath,
|
||||
moveRow, setHoverIndex, viewHelperProps
|
||||
}) {
|
||||
|
||||
const [key, setKey] = useState(false);
|
||||
const depListener = useContext(DepListenerContext);
|
||||
const schemaState = useContext(SchemaStateContext);
|
||||
const rowRef = useRef(null);
|
||||
const dragHandleRef = useRef(null);
|
||||
|
||||
|
@ -150,7 +84,7 @@ function DataTableRow({index, row, totalRows, isResizing, isHovered, schema, sch
|
|||
schemaRef.current.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);
|
||||
schemaState?.addDepListener(accessPath.concat(field.id), accessPath.concat(field.id), field.depChange, field.deferredDepChange);
|
||||
}
|
||||
(evalFunc(null, field.deps) || []).forEach((dep)=>{
|
||||
let source = accessPath.concat(dep);
|
||||
|
@ -158,14 +92,14 @@ function DataTableRow({index, row, totalRows, isResizing, isHovered, schema, sch
|
|||
source = dep;
|
||||
}
|
||||
if(field.depChange) {
|
||||
depListener?.addDepListener(source, accessPath.concat(field.id), field.depChange);
|
||||
schemaState?.addDepListener(source, accessPath.concat(field.id), field.depChange);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return ()=>{
|
||||
/* Cleanup the listeners when unmounting */
|
||||
depListener?.removeDepListener(accessPath);
|
||||
schemaState?.removeDepListener(accessPath);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
@ -227,9 +161,14 @@ function DataTableRow({index, row, totalRows, isResizing, isHovered, schema, sch
|
|||
drop(rowRef);
|
||||
|
||||
return useMemo(()=>
|
||||
<PgReactTableRowContent ref={rowRef} data-handler-id={handlerId} className={isHovered ? 'DataGridView-tableRowHovered' : null} data-test='data-table-row' style={{position: 'initial'}}>
|
||||
<PgReactTableRowContent ref={rowRef} data-handler-id={handlerId}
|
||||
className={isHovered ? 'DataGridView-tableRowHovered' : null}
|
||||
data-test='data-table-row' style={{position: 'initial'}}>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
let {modeSupported} = cell.column.field ? getFieldMetaData(cell.column.field, schemaRef.current, {}, viewHelperProps) : {modeSupported: true};
|
||||
// Let's not render the cell, which are not supported in this mode.
|
||||
if (cell.column.field && !isModeSupportedByField(
|
||||
cell.column.field, viewHelperProps
|
||||
)) return;
|
||||
|
||||
const content = flexRender(cell.column.columnDef.cell, {
|
||||
key: cell.column.columnDef.cell?.type ?? cell.column.columnDef.id,
|
||||
|
@ -237,8 +176,9 @@ function DataTableRow({index, row, totalRows, isResizing, isHovered, schema, sch
|
|||
reRenderRow: ()=>{setKey((currKey)=>!currKey);}
|
||||
});
|
||||
|
||||
return (modeSupported &&
|
||||
<PgReactTableCell cell={cell} row={row} key={cell.id} ref={cell.column.id == 'btn-reorder' ? dragHandleRef : null}>
|
||||
return (
|
||||
<PgReactTableCell cell={cell} row={row} key={cell.id}
|
||||
ref={cell.column.id == 'btn-reorder' ? dragHandleRef : null}>
|
||||
{content}
|
||||
</PgReactTableCell>
|
||||
);
|
||||
|
@ -337,9 +277,10 @@ function getMappedCell({
|
|||
|
||||
export default function DataGridView({
|
||||
value, viewHelperProps, schema, accessPath, dataDispatch, containerClassName,
|
||||
fixedRows, ...props}) {
|
||||
fixedRows, ...props
|
||||
}) {
|
||||
|
||||
const stateUtils = useContext(StateUtilsContext);
|
||||
const schemaState = useContext(SchemaStateContext);
|
||||
const checkIsMounted = useIsMounted();
|
||||
const [hoverIndex, setHoverIndex] = useState();
|
||||
const newRowIndex = useRef();
|
||||
|
@ -439,14 +380,14 @@ export default function DataGridView({
|
|||
}
|
||||
|
||||
cols = cols.concat(
|
||||
schemaRef.current.fields.filter((f)=>{
|
||||
return _.isArray(props.columns) ? props.columns.indexOf(f.id) > -1 : true;
|
||||
}).sort((firstF, secondF)=>{
|
||||
if(_.isArray(props.columns)) {
|
||||
return props.columns.indexOf(firstF.id) < props.columns.indexOf(secondF.id) ? -1 : 1;
|
||||
}
|
||||
return 0;
|
||||
}).map((field)=>{
|
||||
schemaRef.current.fields.filter((f) => (
|
||||
_.isArray(props.columns) ? props.columns.indexOf(f.id) > -1 : true
|
||||
)).sort((firstF, secondF) => (
|
||||
_.isArray(props.columns) ? ((
|
||||
props.columns.indexOf(firstF.id) <
|
||||
props.columns.indexOf(secondF.id)
|
||||
) ? -1 : 1) : 0
|
||||
)).map((field) => {
|
||||
let widthParms = {};
|
||||
if(field.width) {
|
||||
widthParms.size = field.width;
|
||||
|
@ -461,7 +402,10 @@ export default function DataGridView({
|
|||
if(field.maxWidth) {
|
||||
widthParms.maxSize = field.maxWidth;
|
||||
}
|
||||
widthParms.enableResizing = _.isUndefined(field.enableResizing) ? true : Boolean(field.enableResizing);
|
||||
widthParms.enableResizing =
|
||||
_.isUndefined(field.enableResizing) ? true : Boolean(
|
||||
field.enableResizing
|
||||
);
|
||||
|
||||
let colInfo = {
|
||||
header: field.label||<> </>,
|
||||
|
@ -490,8 +434,7 @@ export default function DataGridView({
|
|||
const ret = {};
|
||||
|
||||
columns.forEach(column => {
|
||||
let {modeSupported} = column.field ? getFieldMetaData(column.field, schemaRef.current, {}, viewHelperProps) : {modeSupported: true};
|
||||
ret[column.id] = modeSupported;
|
||||
ret[column.id] = isModeSupportedByField(column.field, viewHelperProps);
|
||||
});
|
||||
|
||||
return ret;
|
||||
|
@ -538,16 +481,17 @@ export default function DataGridView({
|
|||
useEffect(() => {
|
||||
let rowsPromise = fixedRows;
|
||||
|
||||
/* If fixedRows is defined, fetch the details */
|
||||
// If fixedRows is defined, fetch the details.
|
||||
if(typeof rowsPromise === 'function') {
|
||||
rowsPromise = rowsPromise();
|
||||
}
|
||||
|
||||
if(rowsPromise) {
|
||||
Promise.resolve(rowsPromise)
|
||||
.then((res) => {
|
||||
/* If component unmounted, dont update state */
|
||||
if(checkIsMounted()) {
|
||||
stateUtils.initOrigData(accessPath, res);
|
||||
schemaState.setUnpreparedData(accessPath, res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -559,11 +503,16 @@ export default function DataGridView({
|
|||
|
||||
// Try autofocus on newly added row.
|
||||
setTimeout(() => {
|
||||
const rowInput = tableRef.current?.querySelector(`.pgrt-row[data-index="${newRowIndex.current}"] input`);
|
||||
const rowInput = tableRef.current?.querySelector(
|
||||
`.pgrt-row[data-index="${newRowIndex.current}"] input`
|
||||
);
|
||||
if(!rowInput) return;
|
||||
|
||||
requestAnimationAndFocus(tableRef.current.querySelector(`.pgrt-row[data-index="${newRowIndex.current}"] input`));
|
||||
props.expandEditOnAdd && props.canEdit && rows[newRowIndex.current]?.toggleExpanded(true);
|
||||
requestAnimationAndFocus(tableRef.current.querySelector(
|
||||
`.pgrt-row[data-index="${newRowIndex.current}"] input`
|
||||
));
|
||||
props.expandEditOnAdd && props.canEdit &&
|
||||
rows[newRowIndex.current]?.toggleExpanded(true);
|
||||
newRowIndex.current = undefined;
|
||||
}, 50);
|
||||
}
|
||||
|
@ -599,7 +548,7 @@ export default function DataGridView({
|
|||
}
|
||||
|
||||
return (
|
||||
<StyledBox className={containerClassName}>
|
||||
<StyleDataGridBox className={containerClassName}>
|
||||
<Box className='DataGridView-grid'>
|
||||
{(props.label || props.canAdd) && <DataGridHeader label={props.label} canAdd={props.canAdd} onAddClick={onAddClick}
|
||||
canSearch={props.canSearch}
|
||||
|
@ -639,7 +588,7 @@ export default function DataGridView({
|
|||
</PgReactTable>
|
||||
</DndProvider>
|
||||
</Box>
|
||||
</StyledBox>
|
||||
</StyleDataGridBox>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -7,11 +7,9 @@
|
|||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
|
||||
export const DepListenerContext = React.createContext();
|
||||
|
||||
export default class DepListener {
|
||||
export class DepListener {
|
||||
constructor() {
|
||||
this._depListeners = [];
|
||||
}
|
||||
|
|
|
@ -8,30 +8,39 @@
|
|||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
|
||||
import Grid from '@mui/material/Grid';
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { MappedFormControl } from './MappedControl';
|
||||
import { SCHEMA_STATE_ACTIONS, StateUtilsContext } from '.';
|
||||
import FieldSet from 'sources/components/FieldSet';
|
||||
import CustomPropTypes from 'sources/custom_prop_types';
|
||||
import { evalFunc } from 'sources/utils';
|
||||
import CustomPropTypes from '../custom_prop_types';
|
||||
import { DepListenerContext } from './DepListener';
|
||||
import { getFieldMetaData } from './FormView';
|
||||
import FieldSet from '../components/FieldSet';
|
||||
import { Grid } from '@mui/material';
|
||||
|
||||
import { MappedFormControl } from './MappedControl';
|
||||
import {
|
||||
getFieldMetaData, SCHEMA_STATE_ACTIONS, SchemaStateContext
|
||||
} from './common';
|
||||
|
||||
|
||||
const INLINE_COMPONENT_ROWGAP = '8px';
|
||||
|
||||
export default function FieldSetView({
|
||||
value, schema={}, viewHelperProps, accessPath, dataDispatch, controlClassName, isDataGridForm=false, label, visible}) {
|
||||
const depListener = useContext(DepListenerContext);
|
||||
const stateUtils = useContext(StateUtilsContext);
|
||||
value, schema={}, viewHelperProps, accessPath, dataDispatch,
|
||||
controlClassName, isDataGridForm=false, label, visible
|
||||
}) {
|
||||
const schemaState = useContext(SchemaStateContext);
|
||||
|
||||
useEffect(() => {
|
||||
/* Calculate the fields which depends on the current field */
|
||||
if(!isDataGridForm && depListener) {
|
||||
// Calculate the fields which depends on the current field.
|
||||
if(!isDataGridForm && schemaState) {
|
||||
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);
|
||||
schemaState?.addDepListener(
|
||||
accessPath.concat(field.id), accessPath.concat(field.id),
|
||||
field.depChange, field.deferredDepChange
|
||||
);
|
||||
}
|
||||
(evalFunc(null, field.deps) || []).forEach((dep) => {
|
||||
let source = accessPath.concat(dep);
|
||||
|
@ -39,7 +48,9 @@ export default function FieldSetView({
|
|||
source = dep;
|
||||
}
|
||||
if(field.depChange) {
|
||||
depListener.addDepListener(source, accessPath.concat(field.id), field.depChange);
|
||||
schemaState?.addDepListener(
|
||||
source, accessPath.concat(field.id), field.depChange
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -49,17 +60,25 @@ export default function FieldSetView({
|
|||
let viewFields = [];
|
||||
let inlineComponents = [];
|
||||
|
||||
/* Prepare the array of components based on the types */
|
||||
for(const field of schema.fields) {
|
||||
let {visible, disabled, readonly, modeSupported} =
|
||||
getFieldMetaData(field, schema, value, viewHelperProps);
|
||||
if(!visible) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if(modeSupported) {
|
||||
/* Its a form control */
|
||||
const hasError = field.id == stateUtils?.formErr.name;
|
||||
/* When there is a change, the dependent values can change
|
||||
* lets pass the new changes to dependent and get the new values
|
||||
* from there as well.
|
||||
// Prepare the array of components based on the types.
|
||||
for(const field of schema.fields) {
|
||||
const {
|
||||
visible, disabled, readonly, modeSupported
|
||||
} = getFieldMetaData(field, schema, value, viewHelperProps);
|
||||
|
||||
if(!modeSupported) continue;
|
||||
|
||||
// Its a form control.
|
||||
const hasError = (field.id === schemaState?.errors.name);
|
||||
|
||||
/*
|
||||
* When there is a change, the dependent values can also change.
|
||||
* Let's pass these changes to dependent for take them into effect to
|
||||
* generate new values.
|
||||
*/
|
||||
const currentControl = <MappedFormControl
|
||||
state={value}
|
||||
|
@ -101,7 +120,8 @@ export default function FieldSetView({
|
|||
withContainer: false, controlGridBasis: 3
|
||||
}));
|
||||
viewFields.push(
|
||||
<Grid container spacing={0} key={`ic-${inlineComponents[0].key}`} className={controlClassName} rowGap="8px">
|
||||
<Grid container spacing={0} key={`ic-${inlineComponents[0].key}`}
|
||||
className={controlClassName} rowGap={INLINE_COMPONENT_ROWGAP}>
|
||||
{inlineComponents}
|
||||
</Grid>
|
||||
);
|
||||
|
@ -110,19 +130,16 @@ export default function FieldSetView({
|
|||
viewFields.push(currentControl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(inlineComponents?.length > 0) {
|
||||
viewFields.push(
|
||||
<Grid container spacing={0} key={`ic-${inlineComponents[0].key}`} className={controlClassName} rowGap="8px">
|
||||
<Grid container spacing={0} key={`ic-${inlineComponents[0].key}`}
|
||||
className={controlClassName} rowGap={INLINE_COMPONENT_ROWGAP}>
|
||||
{inlineComponents}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
if(!visible) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<FieldSet title={label} className={controlClassName}>
|
||||
{viewFields}
|
||||
|
|
|
@ -7,69 +7,29 @@
|
|||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import React, {
|
||||
useContext, useEffect, useMemo, useRef, useState
|
||||
} from 'react';
|
||||
import { Box, Tab, Tabs, Grid } from '@mui/material';
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { MappedFormControl } from './MappedControl';
|
||||
import TabPanel from '../components/TabPanel';
|
||||
import DataGridView from './DataGridView';
|
||||
import { SCHEMA_STATE_ACTIONS, StateUtilsContext } from '.';
|
||||
import { FormNote, InputSQL } from '../components/FormComponents';
|
||||
import { FormNote, InputSQL } from 'sources/components/FormComponents';
|
||||
import TabPanel from 'sources/components/TabPanel';
|
||||
import { useOnScreen } from 'sources/custom_hooks';
|
||||
import CustomPropTypes from 'sources/custom_prop_types';
|
||||
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';
|
||||
import FieldSetView from './FieldSetView';
|
||||
|
||||
const StyledBox = styled(Box)(({theme}) => ({
|
||||
'& .FormView-nestedControl': {
|
||||
height: 'unset !important',
|
||||
'& .FormView-controlRow': {
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
'& .FormView-nestedTabPanel': {
|
||||
backgroundColor: theme.otherVars.headerBg,
|
||||
}
|
||||
},
|
||||
'& .FormView-errorMargin': {
|
||||
/* Error footer space */
|
||||
paddingBottom: '36px !important',
|
||||
},
|
||||
'& .FormView-fullSpace': {
|
||||
padding: '0 !important',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
'& .FormView-fullControl': {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
'& .FormView-sqlTabInput': {
|
||||
border: 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
'& .FormView-nonTabPanel': {
|
||||
...theme.mixins.tabPanel,
|
||||
'& .FormView-nonTabPanelContent': {
|
||||
height: 'unset',
|
||||
'& .FormView-controlRow': {
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
}
|
||||
},
|
||||
'& .FormView-singleCollectionPanel': {
|
||||
...theme.mixins.tabPanel,
|
||||
'& .FormView-singleCollectionPanelContent': {
|
||||
'& .FormView-controlRow': {
|
||||
marginBottom: theme.spacing(1),
|
||||
height: '100%',
|
||||
},
|
||||
}
|
||||
},
|
||||
}));
|
||||
import DataGridView from './DataGridView';
|
||||
import { MappedFormControl } from './MappedControl';
|
||||
import FieldSetView from './FieldSetView';
|
||||
import {
|
||||
SCHEMA_STATE_ACTIONS, SchemaStateContext, getFieldMetaData
|
||||
} from './common';
|
||||
|
||||
import { FormContentBox } from './StyledComponents';
|
||||
|
||||
|
||||
/* Optional SQL tab */
|
||||
function SQLTab({active, getSQLValue}) {
|
||||
|
@ -102,77 +62,12 @@ SQLTab.propTypes = {
|
|||
getSQLValue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export function getFieldMetaData(field, schema, value, viewHelperProps, onlyModeCheck=false) {
|
||||
let retData = {
|
||||
readonly: false,
|
||||
disabled: false,
|
||||
visible: true,
|
||||
editable: true,
|
||||
canAdd: true,
|
||||
canEdit: false,
|
||||
canDelete: true,
|
||||
modeSupported: true,
|
||||
canAddRow: true,
|
||||
};
|
||||
|
||||
if(field.mode) {
|
||||
retData.modeSupported = (field.mode.indexOf(viewHelperProps.mode) > -1);
|
||||
}
|
||||
if(!retData.modeSupported) {
|
||||
return retData;
|
||||
}
|
||||
|
||||
if(onlyModeCheck) {
|
||||
return retData;
|
||||
}
|
||||
|
||||
let {visible, disabled, readonly, editable} = field;
|
||||
let verInLimit;
|
||||
|
||||
if (_.isUndefined(viewHelperProps.serverInfo)) {
|
||||
verInLimit= true;
|
||||
} else {
|
||||
verInLimit = ((_.isUndefined(field.server_type) ? true :
|
||||
(viewHelperProps.serverInfo.type in field.server_type)) &&
|
||||
(_.isUndefined(field.min_version) ? true :
|
||||
(viewHelperProps.serverInfo.version >= field.min_version)) &&
|
||||
(_.isUndefined(field.max_version) ? true :
|
||||
(viewHelperProps.serverInfo.version <= field.max_version)));
|
||||
}
|
||||
|
||||
retData.readonly = viewHelperProps.inCatalog || (viewHelperProps.mode == 'properties');
|
||||
if(!retData.readonly) {
|
||||
retData.readonly = evalFunc(schema, readonly, value);
|
||||
}
|
||||
|
||||
let _visible = verInLimit;
|
||||
_visible = _visible && evalFunc(schema, _.isUndefined(visible) ? true : visible, value);
|
||||
retData.visible = Boolean(_visible);
|
||||
|
||||
retData.disabled = Boolean(evalFunc(schema, disabled, value));
|
||||
|
||||
retData.editable = !(viewHelperProps.inCatalog || (viewHelperProps.mode == 'properties'));
|
||||
if(retData.editable) {
|
||||
retData.editable = evalFunc(schema, _.isUndefined(editable) ? true : editable, value);
|
||||
}
|
||||
|
||||
let {canAdd, canEdit, canDelete, canReorder, canAddRow } = field;
|
||||
retData.canAdd = _.isUndefined(canAdd) ? retData.canAdd : evalFunc(schema, canAdd, value);
|
||||
retData.canAdd = !retData.disabled && retData.canAdd;
|
||||
retData.canEdit = _.isUndefined(canEdit) ? retData.canEdit : evalFunc(schema, canEdit, value);
|
||||
retData.canEdit = !retData.disabled && retData.canEdit;
|
||||
retData.canDelete = _.isUndefined(canDelete) ? retData.canDelete : evalFunc(schema, canDelete, value);
|
||||
retData.canDelete = !retData.disabled && retData.canDelete;
|
||||
retData.canReorder =_.isUndefined(canReorder) ? retData.canReorder : evalFunc(schema, canReorder, value);
|
||||
retData.canAddRow = _.isUndefined(canAddRow) ? retData.canAddRow : evalFunc(schema, canAddRow, value);
|
||||
return retData;
|
||||
}
|
||||
|
||||
/* The first component of schema view form */
|
||||
export default function FormView({
|
||||
value, schema={}, viewHelperProps, isNested=false, accessPath, dataDispatch, hasSQLTab,
|
||||
getSQLValue, onTabChange, firstEleRef, className, isDataGridForm=false, isTabView=true, visible}) {
|
||||
let defaultTab = 'General';
|
||||
let defaultTab = gettext('General');
|
||||
let tabs = {};
|
||||
let tabsClassname = {};
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
|
@ -180,10 +75,9 @@ export default function FormView({
|
|||
const firstEleID = useRef();
|
||||
const formRef = useRef();
|
||||
const onScreenTracker = useRef(false);
|
||||
const depListener = useContext(DepListenerContext);
|
||||
let groupLabels = {};
|
||||
const schemaRef = useRef(schema);
|
||||
const stateUtils = useContext(StateUtilsContext);
|
||||
const schemaState = useContext(SchemaStateContext);
|
||||
|
||||
let isOnScreen = useOnScreen(formRef);
|
||||
|
||||
|
@ -206,7 +100,7 @@ export default function FormView({
|
|||
schemaRef.current.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);
|
||||
schemaState?.addDepListener(accessPath.concat(field.id), accessPath.concat(field.id), field.depChange, field.deferredDepChange);
|
||||
}
|
||||
(evalFunc(null, field.deps) || []).forEach((dep)=>{
|
||||
// when dep is a string then prepend the complete accessPath
|
||||
|
@ -217,24 +111,25 @@ export default function FormView({
|
|||
source = dep;
|
||||
}
|
||||
if(field.depChange || field.deferredDepChange) {
|
||||
depListener.addDepListener(source, accessPath.concat(field.id), field.depChange, field.deferredDepChange);
|
||||
schemaState?.addDepListener(source, accessPath.concat(field.id), field.depChange, field.deferredDepChange);
|
||||
}
|
||||
if(field.depChange || field.deferredDepChange) {
|
||||
depListener.addDepListener(source, accessPath.concat(field.id), field.depChange, field.deferredDepChange);
|
||||
schemaState?.addDepListener(source, accessPath.concat(field.id), field.depChange, field.deferredDepChange);
|
||||
}
|
||||
});
|
||||
});
|
||||
return ()=>{
|
||||
/* Cleanup the listeners when unmounting */
|
||||
depListener.removeDepListener(accessPath);
|
||||
schemaState?.removeDepListener(accessPath);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
/* Upon reset, set the tab to first */
|
||||
useEffect(()=>{
|
||||
if (schemaState?.isReady)
|
||||
setTabValue(0);
|
||||
}, [stateUtils.formResetKey]);
|
||||
}, [schemaState?.isReady]);
|
||||
|
||||
let fullTabs = [];
|
||||
let inlineComponents = [];
|
||||
|
@ -242,23 +137,29 @@ export default function FormView({
|
|||
|
||||
/* Prepare the array of components based on the types */
|
||||
for(const field of schemaRef.current.fields) {
|
||||
let {visible, disabled, readonly, canAdd, canEdit, canDelete, canReorder, canAddRow, modeSupported} =
|
||||
getFieldMetaData(field, schema, value, viewHelperProps);
|
||||
let {
|
||||
visible, disabled, readonly, canAdd, canEdit, canDelete, canReorder,
|
||||
canAddRow, modeSupported
|
||||
} = getFieldMetaData(field, schema, value, viewHelperProps);
|
||||
|
||||
if(!modeSupported) continue;
|
||||
|
||||
if(modeSupported) {
|
||||
let {group, CustomControl} = field;
|
||||
|
||||
if(field.type === 'group') {
|
||||
groupLabels[field.id] = field.label;
|
||||
|
||||
if(!visible) {
|
||||
schemaRef.current.filterGroups.push(field.label);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
group = groupLabels[group] || group || defaultTab;
|
||||
|
||||
if(!tabs[group]) tabs[group] = [];
|
||||
|
||||
/* Lets choose the path based on type */
|
||||
// Lets choose the path based on type.
|
||||
if(field.type === 'nested-tab') {
|
||||
/* Pass on the top schema */
|
||||
if(isNested) {
|
||||
|
@ -318,7 +219,9 @@ export default function FormView({
|
|||
}
|
||||
} else {
|
||||
/* Its a form control */
|
||||
const hasError = _.isEqual(accessPath.concat(field.id), stateUtils.formErr.name);
|
||||
const hasError = _.isEqual(
|
||||
accessPath.concat(field.id), schemaState.errors?.name
|
||||
);
|
||||
/* When there is a change, the dependent values can change
|
||||
* lets pass the new changes to dependent and get the new values
|
||||
* from there as well.
|
||||
|
@ -392,7 +295,8 @@ export default function FormView({
|
|||
withContainer: false, controlGridBasis: 3
|
||||
}));
|
||||
tabs[group].push(
|
||||
<Grid container spacing={0} key={`ic-${inlineComponents[0].key}`} className='FormView-controlRow' rowGap="8px">
|
||||
<Grid container spacing={0} key={`ic-${inlineComponents[0].key}`}
|
||||
className='FormView-controlRow' rowGap="8px">
|
||||
{inlineComponents}
|
||||
</Grid>
|
||||
);
|
||||
|
@ -403,24 +307,27 @@ export default function FormView({
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(inlineComponents?.length > 0) {
|
||||
tabs[inlineCompGroup].push(
|
||||
<Grid container spacing={0} key={`ic-${inlineComponents[0].key}`} className='FormView-controlRow' rowGap="8px">
|
||||
<Grid container spacing={0} key={`ic-${inlineComponents[0].key}`}
|
||||
className='FormView-controlRow' rowGap="8px">
|
||||
{inlineComponents}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
let finalTabs = _.pickBy(tabs, (v, tabName)=>schemaRef.current.filterGroups.indexOf(tabName) <= -1);
|
||||
let finalTabs = _.pickBy(
|
||||
tabs, (v, tabName) => schemaRef.current.filterGroups.indexOf(tabName) <= -1
|
||||
);
|
||||
|
||||
/* Add the SQL tab if required */
|
||||
// Add the SQL tab (if required)
|
||||
let sqlTabActive = false;
|
||||
let sqlTabName = gettext('SQL');
|
||||
|
||||
if(hasSQLTab) {
|
||||
sqlTabActive = (Object.keys(finalTabs).length === tabValue);
|
||||
/* Re-render and fetch the SQL tab when it is active */
|
||||
// Re-render and fetch the SQL tab when it is active.
|
||||
finalTabs[sqlTabName] = [
|
||||
<SQLTab key="sqltab" active={sqlTabActive} getSQLValue={getSQLValue} />,
|
||||
];
|
||||
|
@ -437,26 +344,25 @@ export default function FormView({
|
|||
// in that case, we could force virtualization of the collection.
|
||||
if(isTabView) return false;
|
||||
|
||||
const visibleEle = Object.values(finalTabs)[0].filter((c)=>c.props.visible);
|
||||
return visibleEle.length == 1
|
||||
&& visibleEle[0]?.type == DataGridView;
|
||||
|
||||
const visibleEle = Object.values(finalTabs)[0].filter(
|
||||
(c) => c.props.visible
|
||||
);
|
||||
return visibleEle.length == 1 && visibleEle[0]?.type == DataGridView;
|
||||
}, [isTabView, finalTabs]);
|
||||
|
||||
/* check whether form is kept hidden by visible prop */
|
||||
// Check whether form is kept hidden by visible prop.
|
||||
if(!_.isUndefined(visible) && !visible) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if(isTabView) {
|
||||
return (
|
||||
<StyledBox height="100%" display="flex" flexDirection="column" className={className} ref={formRef} data-test="form-view">
|
||||
<FormContentBox height="100%" display="flex" flexDirection="column"
|
||||
className={className} ref={formRef} data-test="form-view">
|
||||
<Box>
|
||||
<Tabs
|
||||
value={tabValue}
|
||||
onChange={(event, selTabValue) => {
|
||||
setTabValue(selTabValue);
|
||||
}}
|
||||
onChange={(event, selTabValue) => { setTabValue(selTabValue); }}
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
action={(ref)=>ref?.updateIndicator()}
|
||||
|
@ -467,34 +373,49 @@ export default function FormView({
|
|||
</Tabs>
|
||||
</Box>
|
||||
{Object.keys(finalTabs).map((tabName, i)=>{
|
||||
let contentClassName = [(stateUtils.formErr.message ? 'FormView-errorMargin': null)];
|
||||
let contentClassName = [(
|
||||
schemaState.errors?.message ? 'FormView-errorMargin': null
|
||||
)];
|
||||
|
||||
if(fullTabs.indexOf(tabName) == -1) {
|
||||
contentClassName.push('FormView-nestedControl');
|
||||
} else {
|
||||
contentClassName.push('FormView-fullControl');
|
||||
}
|
||||
|
||||
return (
|
||||
<TabPanel key={tabName} value={tabValue} index={i} classNameRoot={[tabsClassname[tabName], (isNested ? 'FormView-nestedTabPanel' : null)].join(' ')}
|
||||
<TabPanel key={tabName} value={tabValue} index={i}
|
||||
classNameRoot={[
|
||||
tabsClassname[tabName],
|
||||
(isNested ? 'FormView-nestedTabPanel' : null)
|
||||
].join(' ')}
|
||||
className={contentClassName.join(' ')} data-testid={tabName}>
|
||||
{finalTabs[tabName]}
|
||||
</TabPanel>
|
||||
);
|
||||
})}
|
||||
</StyledBox>
|
||||
</FormContentBox>
|
||||
);
|
||||
} else {
|
||||
let contentClassName = [isSingleCollection ? 'FormView-singleCollectionPanelContent' : 'FormView-nonTabPanelContent', (stateUtils.formErr.message ? 'FormView-errorMargin' : null)];
|
||||
let contentClassName = [
|
||||
isSingleCollection ? 'FormView-singleCollectionPanelContent' :
|
||||
'FormView-nonTabPanelContent',
|
||||
(schemaState.errors?.message ? 'FormView-errorMargin' : null)
|
||||
];
|
||||
return (
|
||||
<StyledBox height="100%" display="flex" flexDirection="column" className={className} ref={formRef} data-test="form-view">
|
||||
<FormContentBox height="100%" display="flex" flexDirection="column" className={className} ref={formRef} data-test="form-view">
|
||||
<TabPanel value={tabValue} index={0} classNameRoot={[isSingleCollection ? 'FormView-singleCollectionPanel' : 'FormView-nonTabPanel',className].join(' ')}
|
||||
className={contentClassName.join(' ')}>
|
||||
{Object.keys(finalTabs).map((tabName) => {
|
||||
return (
|
||||
<React.Fragment key={tabName}>{finalTabs[tabName]}</React.Fragment>
|
||||
<React.Fragment key={tabName}>
|
||||
{finalTabs[tabName]}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</TabPanel>
|
||||
</StyledBox>);
|
||||
</FormContentBox>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,301 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, {
|
||||
useCallback, useEffect, useRef, useState,
|
||||
} from 'react';
|
||||
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import DoneIcon from '@mui/icons-material/Done';
|
||||
import InfoIcon from '@mui/icons-material/InfoRounded';
|
||||
import HelpIcon from '@mui/icons-material/HelpRounded';
|
||||
import PublishIcon from '@mui/icons-material/Publish';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import SettingsBackupRestoreIcon from
|
||||
'@mui/icons-material/SettingsBackupRestore';
|
||||
import Box from '@mui/material/Box';
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { parseApiError } from 'sources/api_instance';
|
||||
import { usePgAdmin } from 'sources/BrowserComponent';
|
||||
import Loader from 'sources/components/Loader';
|
||||
import { useIsMounted } from 'sources/custom_hooks';
|
||||
import {
|
||||
PrimaryButton, DefaultButton, PgIconButton
|
||||
} from 'sources/components/Buttons';
|
||||
import {
|
||||
FormFooterMessage, MESSAGE_TYPE
|
||||
} from 'sources/components/FormComponents';
|
||||
import CustomPropTypes from 'sources/custom_prop_types';
|
||||
import gettext from 'sources/gettext';
|
||||
|
||||
import FormView from './FormView';
|
||||
import { StyledBox } from './StyledComponents';
|
||||
import { useSchemaState } from './useSchemaState';
|
||||
import {
|
||||
getForQueryParams, SchemaStateContext
|
||||
} from './common';
|
||||
|
||||
|
||||
/* If its the dialog */
|
||||
export default function SchemaDialogView({
|
||||
getInitData, viewHelperProps, loadingText, schema={}, showFooter=true,
|
||||
isTabView=true, checkDirtyOnEnableSave=false, ...props
|
||||
}) {
|
||||
// View helper properties
|
||||
const { mode, keepCid } = viewHelperProps;
|
||||
const onDataChange = props.onDataChange;
|
||||
|
||||
// Message to the user on long running operations.
|
||||
const [loaderText, setLoaderText] = useState('');
|
||||
|
||||
// Schema data state manager
|
||||
const {schemaState, dataDispatch, sessData, reset} = useSchemaState({
|
||||
schema: schema, getInitData: getInitData, immutableData: {},
|
||||
mode: mode, keepCid: keepCid, onDataChange: onDataChange,
|
||||
});
|
||||
|
||||
const [{isNew, isDirty, isReady, errors}, updateSchemaState] = useState({
|
||||
isNew: true, isDirty: false, isReady: false, errors: {}
|
||||
});
|
||||
|
||||
// Is saving operation in progress?
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// First element to be set by the FormView to set the focus after loading
|
||||
// the data.
|
||||
const firstEleRef = useRef();
|
||||
const checkIsMounted = useIsMounted();
|
||||
const [data, setData] = useState({});
|
||||
|
||||
// Notifier object.
|
||||
const pgAdmin = usePgAdmin();
|
||||
const Notifier = props.Notifier || pgAdmin.Browser.notifier;
|
||||
|
||||
useEffect(() => {
|
||||
/*
|
||||
* Docker on load focusses itself, so our focus should execute later.
|
||||
*/
|
||||
let focusTimeout = setTimeout(()=>{
|
||||
firstEleRef.current?.focus();
|
||||
}, 250);
|
||||
|
||||
// Clear the focus timeout if unmounted.
|
||||
return () => {
|
||||
clearTimeout(focusTimeout);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setLoaderText(schemaState.message);
|
||||
}, [schemaState.message]);
|
||||
|
||||
useEffect(() => {
|
||||
setData(sessData);
|
||||
updateSchemaState(schemaState);
|
||||
}, [sessData.__changeId]);
|
||||
|
||||
const onResetClick = () => {
|
||||
const resetIt = () => {
|
||||
firstEleRef.current?.focus();
|
||||
reset();
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!props.confirmOnCloseReset) {
|
||||
resetIt();
|
||||
return;
|
||||
}
|
||||
|
||||
Notifier.confirm(
|
||||
gettext('Warning'),
|
||||
gettext('Changes will be lost. Are you sure you want to reset?'),
|
||||
resetIt, () => (true),
|
||||
);
|
||||
};
|
||||
|
||||
const save = (changeData) => {
|
||||
props.onSave(isNew, changeData)
|
||||
.then(()=>{
|
||||
if(schema.informText) {
|
||||
Notifier.alert(
|
||||
gettext('Warning'),
|
||||
schema.informText,
|
||||
);
|
||||
}
|
||||
}).catch((err)=>{
|
||||
schemaState.setError({
|
||||
name: 'apierror',
|
||||
message: _.escape(parseApiError(err)),
|
||||
});
|
||||
}).finally(()=>{
|
||||
if(checkIsMounted()) {
|
||||
setSaving(false);
|
||||
setLoaderText('');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onSaveClick = () => {
|
||||
// Do nothing when there is no change or there is an error
|
||||
if (!schemaState.changes || errors.name) return;
|
||||
|
||||
setSaving(true);
|
||||
setLoaderText('Saving...');
|
||||
|
||||
if (!schema.warningText) {
|
||||
save(schemaState.changes);
|
||||
return;
|
||||
}
|
||||
|
||||
Notifier.confirm(
|
||||
gettext('Warning'),
|
||||
schema.warningText,
|
||||
()=> { save(schemaState.changes); },
|
||||
() => {
|
||||
setSaving(false);
|
||||
setLoaderText('');
|
||||
return true;
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const onErrClose = useCallback(() => {
|
||||
const err = { ...errors, message: '' };
|
||||
// Unset the error message, but not the name.
|
||||
schemaState.setError(err);
|
||||
updateSchemaState({isNew, isDirty, isReady, errors: err});
|
||||
});
|
||||
|
||||
const getSQLValue = () => {
|
||||
// Called when SQL tab is active.
|
||||
if(!isDirty) {
|
||||
return Promise.resolve('-- ' + gettext('No updates.'));
|
||||
}
|
||||
|
||||
if(errors.name) {
|
||||
return Promise.resolve('-- ' + gettext('Definition incomplete.'));
|
||||
}
|
||||
|
||||
const changeData = schemaState.changes;
|
||||
/*
|
||||
* Call the passed incoming getSQLValue func to get the SQL
|
||||
* return of getSQLValue should be a promise.
|
||||
*/
|
||||
return props.getSQLValue(isNew, getForQueryParams(changeData));
|
||||
};
|
||||
|
||||
const getButtonIcon = () => {
|
||||
if(props.customSaveBtnIconType == 'upload') {
|
||||
return <PublishIcon />;
|
||||
} else if(props.customSaveBtnIconType == 'done') {
|
||||
return <DoneIcon />;
|
||||
}
|
||||
return <SaveIcon />;
|
||||
};
|
||||
|
||||
const disableSaveBtn = saving ||
|
||||
!isReady ||
|
||||
!(mode === 'edit' || checkDirtyOnEnableSave ? isDirty : true) ||
|
||||
Boolean(errors.name && errors.name !== 'apierror');
|
||||
|
||||
let ButtonIcon = getButtonIcon();
|
||||
|
||||
/* I am Groot */
|
||||
return (
|
||||
<StyledBox>
|
||||
<SchemaStateContext.Provider value={schemaState}>
|
||||
<Box className='Dialog-form'>
|
||||
<Loader message={loaderText || loadingText}/>
|
||||
<FormView value={data}
|
||||
viewHelperProps={viewHelperProps}
|
||||
schema={schema} accessPath={[]}
|
||||
dataDispatch={dataDispatch}
|
||||
hasSQLTab={props.hasSQL} getSQLValue={getSQLValue}
|
||||
firstEleRef={firstEleRef} isTabView={isTabView}
|
||||
className={props.formClassName} />
|
||||
<FormFooterMessage
|
||||
type={MESSAGE_TYPE.ERROR} message={errors?.message}
|
||||
onClose={onErrClose} />
|
||||
</Box>
|
||||
{showFooter &&
|
||||
<Box className='Dialog-footer'>
|
||||
{
|
||||
(!props.disableSqlHelp || !props.disableDialogHelp) &&
|
||||
<Box>
|
||||
<PgIconButton data-test='sql-help'
|
||||
onClick={()=>props.onHelp(true, isNew)}
|
||||
icon={<InfoIcon />} disabled={props.disableSqlHelp}
|
||||
className='Dialog-buttonMargin'
|
||||
title={ gettext('SQL help for this object type.') }
|
||||
/>
|
||||
<PgIconButton data-test='dialog-help'
|
||||
onClick={()=>props.onHelp(false, isNew)}
|
||||
icon={<HelpIcon />} disabled={props.disableDialogHelp}
|
||||
title={ gettext('Help for this dialog.') }
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
<Box marginLeft='auto'>
|
||||
<DefaultButton data-test='Close' onClick={props.onClose}
|
||||
startIcon={<CloseIcon />} className='Dialog-buttonMargin'>
|
||||
{ gettext('Close') }
|
||||
</DefaultButton>
|
||||
<DefaultButton data-test='Reset' onClick={onResetClick}
|
||||
startIcon={<SettingsBackupRestoreIcon />}
|
||||
disabled={(!isDirty) || saving }
|
||||
className='Dialog-buttonMargin'>
|
||||
{ gettext('Reset') }
|
||||
</DefaultButton>
|
||||
<PrimaryButton data-test='Save' onClick={onSaveClick}
|
||||
startIcon={ButtonIcon}
|
||||
disabled={disableSaveBtn}>{
|
||||
props.customSaveBtnName || gettext('Save')
|
||||
}
|
||||
</PrimaryButton>
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
</SchemaStateContext.Provider>
|
||||
</StyledBox>
|
||||
);
|
||||
}
|
||||
|
||||
SchemaDialogView.propTypes = {
|
||||
getInitData: PropTypes.func,
|
||||
viewHelperProps: PropTypes.shape({
|
||||
mode: PropTypes.string.isRequired,
|
||||
serverInfo: PropTypes.shape({
|
||||
type: PropTypes.string,
|
||||
version: PropTypes.number,
|
||||
}),
|
||||
inCatalog: PropTypes.bool,
|
||||
keepCid: PropTypes.bool,
|
||||
}).isRequired,
|
||||
loadingText: PropTypes.string,
|
||||
schema: CustomPropTypes.schemaUI,
|
||||
onSave: PropTypes.func,
|
||||
onClose: PropTypes.func,
|
||||
onHelp: PropTypes.func,
|
||||
onDataChange: PropTypes.func,
|
||||
confirmOnCloseReset: PropTypes.bool,
|
||||
isTabView: PropTypes.bool,
|
||||
hasSQL: PropTypes.bool,
|
||||
getSQLValue: PropTypes.func,
|
||||
disableSqlHelp: PropTypes.bool,
|
||||
disableDialogHelp: PropTypes.bool,
|
||||
showFooter: PropTypes.bool,
|
||||
resetKey: PropTypes.any,
|
||||
customSaveBtnName: PropTypes.string,
|
||||
customSaveBtnIconType: PropTypes.string,
|
||||
formClassName: CustomPropTypes.className,
|
||||
Notifier: PropTypes.object,
|
||||
checkDirtyOnEnableSave: PropTypes.bool,
|
||||
};
|
|
@ -0,0 +1,214 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import InfoIcon from '@mui/icons-material/InfoRounded';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import Box from '@mui/material/Box';
|
||||
import Accordion from '@mui/material/Accordion';
|
||||
import AccordionSummary from '@mui/material/AccordionSummary';
|
||||
import AccordionDetails from '@mui/material/AccordionDetails';
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { usePgAdmin } from 'sources/BrowserComponent';
|
||||
import gettext from 'sources/gettext';
|
||||
import Loader from 'sources/components/Loader';
|
||||
import { PgIconButton, PgButtonGroup } from 'sources/components/Buttons';
|
||||
import CustomPropTypes from 'sources/custom_prop_types';
|
||||
|
||||
import DataGridView from './DataGridView';
|
||||
import FieldSetView from './FieldSetView';
|
||||
import { MappedFormControl } from './MappedControl';
|
||||
import { useSchemaState } from './useSchemaState';
|
||||
import { getFieldMetaData } from './common';
|
||||
|
||||
import { StyledBox } from './StyledComponents';
|
||||
|
||||
|
||||
/* If its the properties tab */
|
||||
export default function SchemaPropertiesView({
|
||||
getInitData, viewHelperProps, schema={}, updatedData, ...props
|
||||
}) {
|
||||
let defaultTab = 'General';
|
||||
let tabs = {};
|
||||
let tabsClassname = {};
|
||||
let groupLabels = {};
|
||||
const [loaderText, setLoaderText] = useState('');
|
||||
|
||||
const pgAdmin = usePgAdmin();
|
||||
const Notifier = pgAdmin.Browser.notifier;
|
||||
const { mode, keepCid } = viewHelperProps;
|
||||
|
||||
// Schema data state manager
|
||||
const {schemaState, sessData} = useSchemaState({
|
||||
schema: schema, getInitData: getInitData, immutableData: updatedData,
|
||||
mode: mode, keepCid: keepCid, onDataChange: null,
|
||||
});
|
||||
const [data, setData] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
if (schemaState.errors?.response)
|
||||
Notifier.pgRespErrorNotify(schemaState.errors.response);
|
||||
}, [schemaState.errors?.name]);
|
||||
|
||||
useEffect(() => {
|
||||
setData(sessData);
|
||||
}, [sessData.__changeId]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoaderText(schemaState.message);
|
||||
}, [schemaState.message]);
|
||||
|
||||
/* A simple loop to get all the controls for the fields */
|
||||
schema.fields.forEach((field) => {
|
||||
let {group} = field;
|
||||
const {
|
||||
visible, disabled, readonly, modeSupported
|
||||
} = getFieldMetaData(field, schema, data, viewHelperProps);
|
||||
group = group || defaultTab;
|
||||
|
||||
if(field.isFullTab) {
|
||||
tabsClassname[group] = 'Properties-noPadding';
|
||||
}
|
||||
|
||||
if(!modeSupported) return;
|
||||
|
||||
group = groupLabels[group] || group || defaultTab;
|
||||
if (field.helpMessageMode?.indexOf(viewHelperProps.mode) == -1)
|
||||
field.helpMessage = '';
|
||||
|
||||
if(!tabs[group]) tabs[group] = [];
|
||||
|
||||
if(field && field.type === 'nested-fieldset') {
|
||||
tabs[group].push(
|
||||
<FieldSetView
|
||||
key={`nested${tabs[group].length}`}
|
||||
value={data}
|
||||
viewHelperProps={viewHelperProps}
|
||||
schema={field.schema}
|
||||
accessPath={[]}
|
||||
controlClassName='Properties-controlRow'
|
||||
{...field}
|
||||
visible={visible}
|
||||
/>
|
||||
);
|
||||
} else if(field.type === 'collection') {
|
||||
tabs[group].push(
|
||||
<DataGridView
|
||||
key={field.id}
|
||||
viewHelperProps={viewHelperProps}
|
||||
name={field.id}
|
||||
value={data[field.id] || []}
|
||||
schema={field.schema}
|
||||
accessPath={[field.id]}
|
||||
containerClassName='Properties-controlRow'
|
||||
canAdd={false}
|
||||
canEdit={false}
|
||||
canDelete={false}
|
||||
visible={visible}
|
||||
/>
|
||||
);
|
||||
} else if(field.type === 'group') {
|
||||
groupLabels[field.id] = field.label;
|
||||
|
||||
if(!visible) {
|
||||
schema.filterGroups.push(field.label);
|
||||
}
|
||||
} else {
|
||||
tabs[group].push(
|
||||
<MappedFormControl
|
||||
key={field.id}
|
||||
viewHelperProps={viewHelperProps}
|
||||
state={sessData}
|
||||
name={field.id}
|
||||
value={data[field.id]}
|
||||
{...field}
|
||||
readonly={readonly}
|
||||
disabled={disabled}
|
||||
visible={visible}
|
||||
className={field.isFullTab ? null :'Properties-controlRow'}
|
||||
noLabel={field.isFullTab}
|
||||
memoDeps={[
|
||||
data[field.id],
|
||||
'Properties-controlRow',
|
||||
field.isFullTab
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let finalTabs = _.pickBy(
|
||||
tabs, (v, tabName) => schema.filterGroups.indexOf(tabName) <= -1
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledBox>
|
||||
<Loader message={loaderText}/>
|
||||
<Box className='Properties-toolbar'>
|
||||
<PgButtonGroup size="small">
|
||||
<PgIconButton
|
||||
data-test="help" onClick={() => props.onHelp(true, false)}
|
||||
icon={<InfoIcon />} disabled={props.disableSqlHelp}
|
||||
title="SQL help for this object type." />
|
||||
<PgIconButton data-test="edit"
|
||||
onClick={props.onEdit} icon={<EditIcon />}
|
||||
title={gettext('Edit object...')} />
|
||||
</PgButtonGroup>
|
||||
</Box>
|
||||
<Box className={'Properties-form'}>
|
||||
<Box>
|
||||
{Object.keys(finalTabs).map((tabName)=>{
|
||||
let id = tabName.replace(' ', '');
|
||||
return (
|
||||
<Accordion key={id}>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
aria-controls={`${id}-content`}
|
||||
id={`${id}-header`}
|
||||
>
|
||||
{tabName}
|
||||
</AccordionSummary>
|
||||
<AccordionDetails className={tabsClassname[tabName]}>
|
||||
<Box style={{width: '100%'}}>
|
||||
{finalTabs[tabName]}
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
</StyledBox>
|
||||
);
|
||||
}
|
||||
|
||||
SchemaPropertiesView.propTypes = {
|
||||
getInitData: PropTypes.func.isRequired,
|
||||
updatedData: PropTypes.object,
|
||||
viewHelperProps: PropTypes.shape({
|
||||
mode: PropTypes.string.isRequired,
|
||||
serverInfo: PropTypes.shape({
|
||||
type: PropTypes.string,
|
||||
version: PropTypes.number,
|
||||
}),
|
||||
inCatalog: PropTypes.bool,
|
||||
keepCid: PropTypes.bool,
|
||||
}).isRequired,
|
||||
schema: CustomPropTypes.schemaUI,
|
||||
onHelp: PropTypes.func,
|
||||
disableSqlHelp: PropTypes.bool,
|
||||
onEdit: PropTypes.func,
|
||||
resetKey: PropTypes.any,
|
||||
itemNodeData: PropTypes.object
|
||||
};
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ErrorBoundary from 'sources/helpers/ErrorBoundary';
|
||||
|
||||
import SchemaDialogView from './SchemaDialogView';
|
||||
import SchemaPropertiesView from './SchemaPropertiesView';
|
||||
|
||||
|
||||
export default function SchemaView({formType, ...props}) {
|
||||
/* Switch the view based on formType */
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
{
|
||||
formType === 'tab' ?
|
||||
<SchemaPropertiesView {...props}/> : <SchemaDialogView {...props}/>
|
||||
}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
SchemaView.propTypes = {
|
||||
formType: PropTypes.oneOf(['tab', 'dialog']),
|
||||
};
|
|
@ -0,0 +1,170 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import { styled } from '@mui/material/styles';
|
||||
|
||||
export const StyledBox = styled(Box)(({theme}) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
minHeight: 0,
|
||||
'& .Dialog-form': {
|
||||
flexGrow: 1,
|
||||
position: 'relative',
|
||||
minHeight: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
'& .Dialog-footer': {
|
||||
padding: theme.spacing(1),
|
||||
background: theme.otherVars.headerBg,
|
||||
display: 'flex',
|
||||
zIndex: 1010,
|
||||
...theme.mixins.panelBorder.top,
|
||||
'& .Dialog-buttonMargin': {
|
||||
marginRight: '0.5rem',
|
||||
},
|
||||
},
|
||||
'& .Properties-toolbar': {
|
||||
padding: theme.spacing(1),
|
||||
background: theme.palette.background.default,
|
||||
...theme.mixins.panelBorder.bottom,
|
||||
},
|
||||
'& .Properties-form': {
|
||||
padding: theme.spacing(1),
|
||||
overflow: 'auto',
|
||||
flexGrow: 1,
|
||||
'& .Properties-controlRow': {
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
'& .Properties-noPadding': {
|
||||
padding: 0,
|
||||
},
|
||||
}));
|
||||
|
||||
export const StyleDataGridBox = styled(Box)(({theme}) => ({
|
||||
'& .DataGridView-grid': {
|
||||
...theme.mixins.panelBorder,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: 0,
|
||||
height: '100%',
|
||||
'& .DataGridView-gridHeader': {
|
||||
display: 'flex',
|
||||
...theme.mixins.panelBorder.bottom,
|
||||
backgroundColor: theme.otherVars.headerBg,
|
||||
'& .DataGridView-gridHeaderText': {
|
||||
padding: theme.spacing(0.5, 1),
|
||||
fontWeight: theme.typography.fontWeightBold,
|
||||
},
|
||||
'& .DataGridView-gridControls': {
|
||||
marginLeft: 'auto',
|
||||
'& .DataGridView-gridControlsButton': {
|
||||
border: 0,
|
||||
borderRadius: 0,
|
||||
...theme.mixins.panelBorder.left,
|
||||
},
|
||||
},
|
||||
},
|
||||
'& .DataGridView-table': {
|
||||
'&.pgrt-table': {
|
||||
'& .pgrt-body':{
|
||||
'& .pgrt-row': {
|
||||
backgroundColor: theme.otherVars.emptySpaceBg,
|
||||
'& .pgrt-row-content':{
|
||||
'& .pgrd-row-cell': {
|
||||
height: 'auto',
|
||||
padding: theme.spacing(0.5),
|
||||
'&.btn-cell, &.expanded-icon-cell': {
|
||||
padding: '2px 0px'
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
'& .DataGridView-tableRowHovered': {
|
||||
position: 'relative',
|
||||
'& .hover-overlay': {
|
||||
backgroundColor: theme.palette.primary.light,
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
opacity: 0.75,
|
||||
}
|
||||
},
|
||||
'& .DataGridView-resizer': {
|
||||
display: 'inline-block',
|
||||
width: '5px',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
transform: 'translateX(50%)',
|
||||
zIndex: 1,
|
||||
touchAction: 'none',
|
||||
},
|
||||
'& .DataGridView-expandedForm': {
|
||||
border: '1px solid '+theme.palette.grey[400],
|
||||
},
|
||||
'& .DataGridView-expandedIconCell': {
|
||||
backgroundColor: theme.palette.grey[400],
|
||||
borderBottom: 'none',
|
||||
}
|
||||
}));
|
||||
|
||||
export const FormContentBox = styled(Box)(({theme}) => ({
|
||||
'& .FormView-nestedControl': {
|
||||
height: 'unset !important',
|
||||
'& .FormView-controlRow': {
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
'& .FormView-nestedTabPanel': {
|
||||
backgroundColor: theme.otherVars.headerBg,
|
||||
}
|
||||
},
|
||||
'& .FormView-errorMargin': {
|
||||
/* Error footer space */
|
||||
paddingBottom: '36px !important',
|
||||
},
|
||||
'& .FormView-fullSpace': {
|
||||
padding: '0 !important',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
'& .FormView-fullControl': {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
'& .FormView-sqlTabInput': {
|
||||
border: 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
'& .FormView-nonTabPanel': {
|
||||
...theme.mixins.tabPanel,
|
||||
'& .FormView-nonTabPanelContent': {
|
||||
height: 'unset',
|
||||
'& .FormView-controlRow': {
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
}
|
||||
},
|
||||
'& .FormView-singleCollectionPanel': {
|
||||
...theme.mixins.tabPanel,
|
||||
'& .FormView-singleCollectionPanelContent': {
|
||||
'& .FormView-controlRow': {
|
||||
marginBottom: theme.spacing(1),
|
||||
height: '100%',
|
||||
},
|
||||
}
|
||||
},
|
||||
}));
|
|
@ -22,6 +22,9 @@ export default class BaseUISchema {
|
|||
this.filterGroups = []; // If set, these groups will be filtered out
|
||||
this.informText = null; // Inform text to show after save, this only saves it
|
||||
this._top = null;
|
||||
|
||||
this._state = null;
|
||||
this._id = Date.now();
|
||||
}
|
||||
|
||||
/* Top schema is helpful if this is used as child */
|
||||
|
@ -42,8 +45,19 @@ 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 */
|
||||
set state(state) {
|
||||
this._state = state;
|
||||
}
|
||||
|
||||
get state() {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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 || {};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React from 'react';
|
||||
import { evalFunc } from 'sources/utils';
|
||||
|
||||
|
||||
export const SCHEMA_STATE_ACTIONS = {
|
||||
INIT: 'init',
|
||||
SET_VALUE: 'set_value',
|
||||
ADD_ROW: 'add_row',
|
||||
DELETE_ROW: 'delete_row',
|
||||
MOVE_ROW: 'move_row',
|
||||
RERENDER: 'rerender',
|
||||
CLEAR_DEFERRED_QUEUE: 'clear_deferred_queue',
|
||||
DEFERRED_DEPCHANGE: 'deferred_depchange',
|
||||
BULK_UPDATE: 'bulk_update',
|
||||
};
|
||||
|
||||
export const SchemaStateContext = React.createContext();
|
||||
|
||||
export function generateTimeBasedRandomNumberString() {
|
||||
return new Date().getTime() + '' + Math.floor(Math.random() * 1000001);
|
||||
}
|
||||
|
||||
export function isModeSupportedByField(field, helperProps) {
|
||||
if (!field || !field.mode) return true;
|
||||
return (field.mode.indexOf(helperProps.mode) > -1);
|
||||
}
|
||||
|
||||
export function getFieldMetaData(
|
||||
field, schema, value, viewHelperProps
|
||||
) {
|
||||
let retData = {
|
||||
readonly: false,
|
||||
disabled: false,
|
||||
visible: true,
|
||||
editable: true,
|
||||
canAdd: true,
|
||||
canEdit: false,
|
||||
canDelete: true,
|
||||
modeSupported: isModeSupportedByField(field, viewHelperProps),
|
||||
canAddRow: true,
|
||||
};
|
||||
|
||||
if(!retData.modeSupported) {
|
||||
return retData;
|
||||
}
|
||||
|
||||
let {visible, disabled, readonly, editable} = field;
|
||||
let verInLimit;
|
||||
|
||||
if (_.isUndefined(viewHelperProps.serverInfo)) {
|
||||
verInLimit= true;
|
||||
} else {
|
||||
verInLimit = ((_.isUndefined(field.server_type) ? true :
|
||||
(viewHelperProps.serverInfo.type in field.server_type)) &&
|
||||
(_.isUndefined(field.min_version) ? true :
|
||||
(viewHelperProps.serverInfo.version >= field.min_version)) &&
|
||||
(_.isUndefined(field.max_version) ? true :
|
||||
(viewHelperProps.serverInfo.version <= field.max_version)));
|
||||
}
|
||||
|
||||
retData.readonly = viewHelperProps.inCatalog || (viewHelperProps.mode == 'properties');
|
||||
if(!retData.readonly) {
|
||||
retData.readonly = evalFunc(schema, readonly, value);
|
||||
}
|
||||
|
||||
let _visible = verInLimit;
|
||||
_visible = _visible && evalFunc(schema, _.isUndefined(visible) ? true : visible, value);
|
||||
retData.visible = Boolean(_visible);
|
||||
|
||||
retData.disabled = Boolean(evalFunc(schema, disabled, value));
|
||||
|
||||
retData.editable = !(
|
||||
viewHelperProps.inCatalog || (viewHelperProps.mode == 'properties')
|
||||
);
|
||||
if(retData.editable) {
|
||||
retData.editable = evalFunc(
|
||||
schema, (_.isUndefined(editable) ? true : editable), value
|
||||
);
|
||||
}
|
||||
|
||||
let {canAdd, canEdit, canDelete, canReorder, canAddRow } = field;
|
||||
retData.canAdd =
|
||||
_.isUndefined(canAdd) ? retData.canAdd : evalFunc(schema, canAdd, value);
|
||||
retData.canAdd = !retData.disabled && retData.canAdd;
|
||||
retData.canEdit = _.isUndefined(canEdit) ? retData.canEdit : evalFunc(
|
||||
schema, canEdit, value
|
||||
);
|
||||
retData.canEdit = !retData.disabled && retData.canEdit;
|
||||
retData.canDelete = _.isUndefined(canDelete) ? retData.canDelete : evalFunc(
|
||||
schema, canDelete, value
|
||||
);
|
||||
retData.canDelete = !retData.disabled && retData.canDelete;
|
||||
retData.canReorder =
|
||||
_.isUndefined(canReorder) ? retData.canReorder : evalFunc(
|
||||
schema, canReorder, value
|
||||
);
|
||||
retData.canAddRow =
|
||||
_.isUndefined(canAddRow) ? retData.canAddRow : evalFunc(
|
||||
schema, canAddRow, value
|
||||
);
|
||||
|
||||
return retData;
|
||||
}
|
||||
|
||||
/*
|
||||
* Compare the sessData with schema.origData.
|
||||
* schema.origData is set to incoming or default data
|
||||
*/
|
||||
export function isValueEqual(val1, val2) {
|
||||
let attrDefined = (
|
||||
!_.isUndefined(val1) && !_.isUndefined(val2) &&
|
||||
!_.isNull(val1) && !_.isNull(val2)
|
||||
);
|
||||
|
||||
/*
|
||||
* 1. If the orig value was null and new one is empty string, then its a
|
||||
* "no change".
|
||||
* 2. If the orig value and new value are of different datatype but of same
|
||||
* value(numeric) "no change".
|
||||
* 3. If the orig value is undefined or null and new value is boolean false
|
||||
* "no change".
|
||||
*/
|
||||
return (
|
||||
_.isEqual(val1, val2) || (
|
||||
(val1 === null || _.isUndefined(val1)) && val2 === ''
|
||||
) || (
|
||||
(val1 === null || _.isUndefined(val1)) &&
|
||||
typeof(val2) === 'boolean' && !val2
|
||||
) || (
|
||||
attrDefined ? (
|
||||
!_.isObject(val1) && _.isEqual(val1.toString(), val2.toString())
|
||||
) : false
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Compare two objects.
|
||||
*/
|
||||
export function isObjectEqual(val1, val2) {
|
||||
const allKeys = Array.from(
|
||||
new Set([...Object.keys(val1), ...Object.keys(val2)])
|
||||
);
|
||||
return !allKeys.some((k) => {
|
||||
return !isValueEqual(val1[k], val2[k]);
|
||||
});
|
||||
}
|
||||
|
||||
export function getForQueryParams(data) {
|
||||
let retData = {...data};
|
||||
Object.keys(retData).forEach((key)=>{
|
||||
let value = retData[key];
|
||||
if(_.isObject(value) || _.isNull(value)) {
|
||||
retData[key] = JSON.stringify(value);
|
||||
}
|
||||
});
|
||||
return retData;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,333 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import diffArray from 'diff-arrays-of-objects';
|
||||
import _ from 'lodash';
|
||||
|
||||
import gettext from 'sources/gettext';
|
||||
import {
|
||||
minMaxValidator, numberValidator, integerValidator, emptyValidator,
|
||||
checkUniqueCol, isEmptyString
|
||||
} from 'sources/validators';
|
||||
|
||||
import BaseUISchema from './base_schema.ui';
|
||||
import { isModeSupportedByField, isObjectEqual, isValueEqual } from './common';
|
||||
|
||||
// Remove cid key added by prepareData
|
||||
const cleanCid = (coll, keepCid=false) => (
|
||||
(!coll || keepCid) ? coll : coll.map(
|
||||
(o) => _.pickBy(o, (v, k) => (k !== 'cid'))
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
export function getCollectionDiffInEditMode(
|
||||
field, origVal, sessVal, keepCid, parseChanges
|
||||
) {
|
||||
let change = {};
|
||||
|
||||
const id = field.id;
|
||||
const collIdAttr = field.schema.idAttribute;
|
||||
const origColl = _.get(origVal, id) || [];
|
||||
const sessColl = _.get(sessVal, id) || [];
|
||||
/*
|
||||
* Use 'diffArray' package to get the array diff and extract the
|
||||
* info. 'cid' attribute is used to identify the rows uniquely.
|
||||
*/
|
||||
const changeDiff = diffArray(
|
||||
origColl, sessColl || [], 'cid', { compareFunction: isObjectEqual }
|
||||
);
|
||||
|
||||
if(changeDiff.added.length > 0) {
|
||||
change['added'] = cleanCid(changeDiff.added, keepCid);
|
||||
}
|
||||
|
||||
if(changeDiff.removed.length > 0) {
|
||||
change['deleted'] = cleanCid(changeDiff.removed.map((row) => {
|
||||
// Deleted records must be from the original data, not the newly added.
|
||||
return _.find(_.get(origVal, field.id), ['cid', row.cid]);
|
||||
}), keepCid);
|
||||
}
|
||||
|
||||
if(changeDiff.updated.length > 0) {
|
||||
/*
|
||||
* There is a change in collection. Parse further deep to figure
|
||||
* out the exact details.
|
||||
*/
|
||||
let changed = [];
|
||||
|
||||
for(const changedRow of changeDiff.updated) {
|
||||
const rowIndxSess = _.findIndex(
|
||||
_.get(sessVal, id), (row) => (row.cid === changedRow.cid)
|
||||
);
|
||||
const rowIndxOrig = _.findIndex(
|
||||
_.get(origVal, id), (row) => (row.cid==changedRow.cid)
|
||||
);
|
||||
const finalChangedRow = parseChanges(
|
||||
field.schema, _.get(origVal, [id, rowIndxOrig]),
|
||||
_.get(sessVal, [id, rowIndxSess])
|
||||
);
|
||||
|
||||
if(_.isEmpty(finalChangedRow)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* If the 'id' attribute value is present, then only changed keys
|
||||
* can be passed. Otherwise, passing all the keys is useful.
|
||||
*/
|
||||
const idAttrValue = _.get(sessVal, [id, rowIndxSess, collIdAttr]);
|
||||
|
||||
if(_.isUndefined(idAttrValue)) {
|
||||
changed.push({ ...changedRow, ...finalChangedRow });
|
||||
} else {
|
||||
changed.push({ [collIdAttr]: idAttrValue, ...finalChangedRow });
|
||||
}
|
||||
}
|
||||
|
||||
if(changed.length > 0) {
|
||||
change['changed'] = cleanCid(changed, keepCid);
|
||||
}
|
||||
}
|
||||
|
||||
return change;
|
||||
}
|
||||
|
||||
export function getSchemaDataDiff(
|
||||
topSchema, initData, sessData, mode, keepCid,
|
||||
stringify=false, includeSkipChange=true
|
||||
) {
|
||||
const isEditMode = mode === 'edit';
|
||||
|
||||
// This will be executed recursively as data can be nested.
|
||||
let parseChanges = (schema, origVal, sessVal) => {
|
||||
let levelChanges = {};
|
||||
parseChanges.depth =
|
||||
_.isUndefined(parseChanges.depth) ? 0 : (parseChanges.depth + 1);
|
||||
|
||||
/* The comparator and setter */
|
||||
const attrChanged = (id, change, force=false) => {
|
||||
if(isValueEqual(_.get(origVal, id), _.get(sessVal, id)) && !force) {
|
||||
return;
|
||||
}
|
||||
|
||||
change = change || _.get(sessVal, id);
|
||||
|
||||
if(stringify && (_.isArray(change) || _.isObject(change))) {
|
||||
change = JSON.stringify(change);
|
||||
}
|
||||
|
||||
/*
|
||||
* Null values are not passed in URL params, pass it as an empty string.
|
||||
* Nested values does not need this.
|
||||
*/
|
||||
if(_.isNull(change) && parseChanges.depth === 0) {
|
||||
change = '';
|
||||
}
|
||||
|
||||
levelChanges[id] = change;
|
||||
};
|
||||
|
||||
schema.fields.forEach((field) => {
|
||||
/*
|
||||
* If skipChange is true, then field will not be considered for changed
|
||||
* data. This is helpful when 'Save' or 'Reset' should not be enabled on
|
||||
* this field change alone. No change in other behaviour.
|
||||
*/
|
||||
if(field.skipChange && !includeSkipChange) return;
|
||||
|
||||
/*
|
||||
* At this point the schema assignments like top may not have been done,
|
||||
* so - check if mode is supported by this field, or not.
|
||||
*/
|
||||
if (!isModeSupportedByField(field, {mode})) return;
|
||||
|
||||
if(
|
||||
typeof(field.type) === 'string' && field.type.startsWith('nested-')
|
||||
) {
|
||||
/*
|
||||
* Even if its nested, state is on same hierarchical level.
|
||||
* Find the changes and merge.
|
||||
*/
|
||||
levelChanges = {
|
||||
...levelChanges,
|
||||
...parseChanges(field.schema, origVal, sessVal),
|
||||
};
|
||||
} else if(isEditMode && !_.isEqual(
|
||||
_.get(origVal, field.id), _.get(sessVal, field.id)
|
||||
)) {
|
||||
/*
|
||||
* Check for changes only if in edit mode, otherwise - everything can
|
||||
* go through comparator
|
||||
*/
|
||||
if(field.type === 'collection') {
|
||||
const change = getCollectionDiffInEditMode(
|
||||
field, origVal, sessVal, keepCid, parseChanges
|
||||
);
|
||||
|
||||
if(Object.keys(change).length > 0) {
|
||||
attrChanged(field.id, change, true);
|
||||
}
|
||||
} else {
|
||||
attrChanged(field.id);
|
||||
}
|
||||
} else if(!isEditMode) {
|
||||
if(field.type === 'collection') {
|
||||
const origColl = _.get(origVal, field.id) || [];
|
||||
const sessColl = _.get(sessVal, field.id) || [];
|
||||
|
||||
let changeDiff = diffArray(
|
||||
origColl, sessColl, 'cid', {compareFunction: isObjectEqual}
|
||||
);
|
||||
|
||||
// Check the updated changes,when:
|
||||
// 1. These are the fixed rows.
|
||||
// 2. 'canReorder' flag is set to true.
|
||||
if((
|
||||
!_.isUndefined(field.fixedRows) && changeDiff.updated.length > 0
|
||||
) || (
|
||||
_.isUndefined(field.fixedRows) && (
|
||||
changeDiff.added.length > 0 || changeDiff.removed.length > 0 ||
|
||||
changeDiff.updated.length > 0
|
||||
)
|
||||
) || (
|
||||
field.canReorder && _.differenceBy(origColl, sessColl, 'cid')
|
||||
)) {
|
||||
attrChanged(
|
||||
field.id, cleanCid(_.get(sessVal, field.id), keepCid), true
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if(field.canReorder) {
|
||||
changeDiff = diffArray(origColl, sessColl);
|
||||
|
||||
if(changeDiff.updated.length > 0) {
|
||||
attrChanged(
|
||||
field.id, cleanCid(_.get(sessVal, field.id), keepCid), true
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
attrChanged(field.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
parseChanges.depth--;
|
||||
return levelChanges;
|
||||
};
|
||||
|
||||
let res = parseChanges(topSchema, initData, sessData);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export function validateCollectionSchema(
|
||||
field, sessData, accessPath, setError
|
||||
) {
|
||||
const rows = sessData[field.id] || [];
|
||||
const currPath = accessPath.concat(field.id);
|
||||
|
||||
// Validate duplicate rows.
|
||||
const dupInd = checkUniqueCol(rows, field.uniqueCol);
|
||||
|
||||
if(dupInd > 0) {
|
||||
const uniqueColNames = _.filter(
|
||||
field.schema.fields, (uf) => field.uniqueCol.indexOf(uf.id) > -1
|
||||
).map((uf)=>uf.label).join(', ');
|
||||
|
||||
if (isEmptyString(field.label)) {
|
||||
setError(currPath, gettext('%s must be unique.', uniqueColNames));
|
||||
} else {
|
||||
setError(
|
||||
currPath,
|
||||
gettext('%s in %s must be unique.', uniqueColNames, field.label)
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Loop through data.
|
||||
for(const [rownum, row] of rows.entries()) {
|
||||
if(validateSchema(
|
||||
field.schema, row, setError, currPath.concat(rownum), field.label
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function validateSchema(
|
||||
schema, sessData, setError, accessPath=[], collLabel=null
|
||||
) {
|
||||
sessData = sessData || {};
|
||||
|
||||
for(const field of schema.fields) {
|
||||
// Skip id validation
|
||||
if(schema.idAttribute === field.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the field is has nested schema, then validate the child schema.
|
||||
if(field.schema && (field.schema instanceof BaseUISchema)) {
|
||||
// A collection is an array.
|
||||
if(field.type === 'collection') {
|
||||
if (validateCollectionSchema(field, sessData, accessPath, setError))
|
||||
return true;
|
||||
}
|
||||
// A nested schema ? Recurse
|
||||
else if(validateSchema(field.schema, sessData, setError, accessPath)) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// Normal field, default validations.
|
||||
const value = sessData[field.id];
|
||||
|
||||
const fieldPath = accessPath.concat(field.id);
|
||||
|
||||
const setErrorOnMessage = (message) => {
|
||||
if (message) {
|
||||
setError(fieldPath, message);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if(field.noEmpty) {
|
||||
const label = (
|
||||
collLabel && gettext('%s in %s', field.label, collLabel)
|
||||
) || field.noEmptyLabel || field.label;
|
||||
|
||||
if (setErrorOnMessage(emptyValidator(label, value)))
|
||||
return true;
|
||||
}
|
||||
|
||||
if(field.type === 'int') {
|
||||
if (setErrorOnMessage(
|
||||
integerValidator(field.label, value) ||
|
||||
minMaxValidator(field.label, value, field.min, field.max)
|
||||
))
|
||||
return true;
|
||||
} else if(field.type === 'numeric') {
|
||||
if (setErrorOnMessage(
|
||||
numberValidator(field.label, value) ||
|
||||
minMaxValidator(field.label, value, field.min, field.max)
|
||||
))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return schema.validate(
|
||||
sessData, (id, message) => setError(accessPath.concat(id), message)
|
||||
);
|
||||
}
|
|
@ -0,0 +1,481 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useEffect, useReducer } from 'react';
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import { parseApiError } from 'sources/api_instance';
|
||||
import gettext from 'sources/gettext';
|
||||
|
||||
import { DepListener } from './DepListener';
|
||||
import {
|
||||
getSchemaDataDiff,
|
||||
validateSchema,
|
||||
} from './schemaUtils';
|
||||
|
||||
|
||||
export const SchemaStateContext = React.createContext();
|
||||
|
||||
export const SCHEMA_STATE_ACTIONS = {
|
||||
INIT: 'init',
|
||||
SET_VALUE: 'set_value',
|
||||
ADD_ROW: 'add_row',
|
||||
DELETE_ROW: 'delete_row',
|
||||
MOVE_ROW: 'move_row',
|
||||
RERENDER: 'rerender',
|
||||
CLEAR_DEFERRED_QUEUE: 'clear_deferred_queue',
|
||||
DEFERRED_DEPCHANGE: 'deferred_depchange',
|
||||
BULK_UPDATE: 'bulk_update',
|
||||
};
|
||||
|
||||
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),
|
||||
listener: action.listener,
|
||||
});
|
||||
}
|
||||
return newState;
|
||||
};
|
||||
|
||||
const getDeferredDepChange = (currPath, newState, oldState, action) => {
|
||||
if(action.deferredDepChange) {
|
||||
return action.deferredDepChange(currPath, newState, {
|
||||
type: action.type,
|
||||
path: action.path,
|
||||
value: action.value,
|
||||
depChange: action.depChange,
|
||||
oldState: _.cloneDeep(oldState),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* The main function which manipulates the session state based on actions.
|
||||
*
|
||||
* The state is managed based on path array of a particular key.
|
||||
* For Eg. if the state is
|
||||
* {
|
||||
* key1: {
|
||||
* ckey1: [
|
||||
* {a: 0, b: 0},
|
||||
* {a: 1, b: 1}
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* The path for b in first row will be '[key1, ckey1, 0, b]'.
|
||||
* The path for second row of ckey1 will be '[key1, ckey1, 1]'.
|
||||
*
|
||||
* The path for key1 is '[key1]'.
|
||||
* The state starts with path '[]'.
|
||||
*/
|
||||
const sessDataReducer = (state, action) => {
|
||||
let data = _.cloneDeep(state);
|
||||
let rows, cid, deferredList;
|
||||
data.__deferred__ = data.__deferred__ || [];
|
||||
|
||||
switch(action.type) {
|
||||
case SCHEMA_STATE_ACTIONS.INIT:
|
||||
data = action.payload;
|
||||
break;
|
||||
|
||||
case SCHEMA_STATE_ACTIONS.BULK_UPDATE:
|
||||
rows = (_.get(data, action.path)||[]);
|
||||
rows.forEach((row) => { row[action.id] = false; });
|
||||
_.set(data, action.path, rows);
|
||||
break;
|
||||
|
||||
case SCHEMA_STATE_ACTIONS.SET_VALUE:
|
||||
_.set(data, action.path, action.value);
|
||||
// If there is any dep listeners get the changes.
|
||||
data = getDepChange(action.path, data, state, action);
|
||||
deferredList = 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.
|
||||
cid = _.uniqueId('c');
|
||||
action.value['cid'] = cid;
|
||||
if (action.addOnTop) {
|
||||
rows = [].concat(action.value).concat(_.get(data, action.path)||[]);
|
||||
} else {
|
||||
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.MOVE_ROW:
|
||||
rows = _.get(data, action.path)||[];
|
||||
var row = rows[action.oldIndex];
|
||||
rows.splice(action.oldIndex, 1);
|
||||
rows.splice(action.newIndex, 0, row);
|
||||
_.set(data, action.path, rows);
|
||||
break;
|
||||
|
||||
case SCHEMA_STATE_ACTIONS.CLEAR_DEFERRED_QUEUE:
|
||||
data.__deferred__ = [];
|
||||
return data;
|
||||
|
||||
case SCHEMA_STATE_ACTIONS.DEFERRED_DEPCHANGE:
|
||||
data = getDepChange(action.path, data, state, action);
|
||||
break;
|
||||
}
|
||||
|
||||
data.__changeId = (data.__changeId || 0) + 1;
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
function prepareData(val, createMode=false) {
|
||||
if(_.isPlainObject(val)) {
|
||||
_.forIn(val, function (el) {
|
||||
if (_.isObject(el)) {
|
||||
prepareData(el, createMode);
|
||||
}
|
||||
});
|
||||
} else if(_.isArray(val)) {
|
||||
val.forEach(function(el) {
|
||||
if (_.isPlainObject(el)) {
|
||||
/* The each row in collection need to have an id to identify them uniquely
|
||||
This helps in easily getting what has changed */
|
||||
/* Nested collection rows may or may not have idAttribute.
|
||||
So to decide whether row is new or not set, the cid starts with
|
||||
nn (not new) for existing rows. Newly added will start with 'c' (created)
|
||||
*/
|
||||
el['cid'] = createMode ? _.uniqueId('c') : _.uniqueId('nn');
|
||||
prepareData(el, createMode);
|
||||
}
|
||||
});
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
const LOADING_STATE = {
|
||||
INIT: 'initializing',
|
||||
LOADING: 'loading',
|
||||
LOADED: 'loaded',
|
||||
ERROR: 'Error'
|
||||
};
|
||||
|
||||
class SchemaState extends DepListener {
|
||||
|
||||
constructor(
|
||||
schema, getInitData, immutableData, mode, keepCid, onDataChange
|
||||
) {
|
||||
super();
|
||||
|
||||
////// Helper variables
|
||||
|
||||
// BaseUISchema instance
|
||||
this.schema = schema;
|
||||
// Current mode of operation ('create', 'edit', 'properties')
|
||||
this.mode = mode;
|
||||
// Keep the 'cid' object during diff calculations.
|
||||
this.keepcid = keepCid;
|
||||
// Initialization callback
|
||||
this.getInitData = getInitData;
|
||||
// Data change callback
|
||||
this.onDataChange = onDataChange;
|
||||
|
||||
////// State variables
|
||||
|
||||
// Is is ready to be consumed?
|
||||
this.isReady = false;
|
||||
// Diff between the current snapshot and initial data.
|
||||
this.changes = null;
|
||||
// Loading message (if any)
|
||||
this.message = null;
|
||||
// Current Loading state
|
||||
this.loadingState = LOADING_STATE.INIT;
|
||||
this.hasChanges = false;
|
||||
|
||||
////// Schema instance data
|
||||
|
||||
// Initial data after the ready state
|
||||
this.initData = {};
|
||||
// Current state of the data
|
||||
this.data = {};
|
||||
// Immutable data
|
||||
this.immutableData = immutableData;
|
||||
// Current error
|
||||
this.errors = {};
|
||||
// Pre-ready queue
|
||||
this.preReadyQueue = [];
|
||||
|
||||
this._id = Date.now();
|
||||
}
|
||||
|
||||
setError(err) {
|
||||
this.errors = err;
|
||||
}
|
||||
|
||||
setReady(state) {
|
||||
this.isReady = state;
|
||||
}
|
||||
|
||||
setLoadingState(loadingState) {
|
||||
this.loadingState = loadingState;
|
||||
}
|
||||
|
||||
setLoadingMessage(msg) {
|
||||
this.message = msg;
|
||||
}
|
||||
|
||||
// Initialise the data, and fetch the data from the backend (if required).
|
||||
// 'force' flag can be used for reloading the data from the backend.
|
||||
initialise(dataDispatch, force) {
|
||||
let state = this;
|
||||
|
||||
// Don't attempt to initialize again (if it's already in progress).
|
||||
if (
|
||||
state.loadingState !== LOADING_STATE.INIT ||
|
||||
(force && state.loadingState === LOADING_STATE.LOADING)
|
||||
) return;
|
||||
|
||||
state.setLoadingState(LOADING_STATE.LOADING);
|
||||
state.setLoadingMessage(gettext('Loading...'));
|
||||
|
||||
/*
|
||||
* Fetch the data using getInitData(..) callback.
|
||||
* `getInitData(..)` must be present in 'edit' mode.
|
||||
*/
|
||||
if(state.mode === 'edit' && !state.getInitData) {
|
||||
throw new Error('getInitData must be passed for edit');
|
||||
}
|
||||
|
||||
const initDataPromise = state.getInitData?.() ||
|
||||
Promise.resolve({});
|
||||
|
||||
initDataPromise.then((data) => {
|
||||
data = data || {};
|
||||
|
||||
if(state.mode === 'edit') {
|
||||
// Set the origData to incoming data, useful for comparing.
|
||||
state.initData = prepareData({...data, ...state.immutableData});
|
||||
} else {
|
||||
// In create mode, merge with defaults.
|
||||
state.initData = prepareData({
|
||||
...state.schema.defaults, ...data, ...state.immutableData
|
||||
}, true);
|
||||
}
|
||||
|
||||
state.schema.initialise(state.initData);
|
||||
|
||||
dataDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.INIT,
|
||||
payload: state.initData,
|
||||
});
|
||||
|
||||
state.setLoadingState(LOADING_STATE.LOADED);
|
||||
state.setLoadingMessage('');
|
||||
state.setReady(true);
|
||||
}).catch((err) => {
|
||||
state.setLoadingMessage('');
|
||||
state.setError({
|
||||
name: 'apierror',
|
||||
response: err,
|
||||
message: _.escape(parseApiError(err)),
|
||||
});
|
||||
state.setLoadingState(LOADING_STATE.ERROR);
|
||||
state.setReady(true);
|
||||
});
|
||||
}
|
||||
|
||||
validate(sessData) {
|
||||
let state = this,
|
||||
schema = state.schema;
|
||||
|
||||
// If schema does not have the data or does not have any 'onDataChange'
|
||||
// callback, there is no need to validate the current data.
|
||||
if(!state.isReady || !state.onDataChange) return;
|
||||
|
||||
if(
|
||||
!validateSchema(schema, sessData, (path, message) => {
|
||||
message && state.setError({ name: path, message: _.escape(message) });
|
||||
})
|
||||
) state.setError({});
|
||||
|
||||
// Check if anything changed.
|
||||
let dataDiff = getSchemaDataDiff(
|
||||
schema, state.initData, sessData,
|
||||
state.mode, state.keepCid, false, false
|
||||
);
|
||||
const hasDataChanged = state.hasChanges = Object.keys(dataDiff).length > 0;
|
||||
|
||||
// Inform the callbacks about change in the data.
|
||||
if(state.mode !== 'edit') {
|
||||
// Merge the changed data with origData in 'create' mode.
|
||||
dataDiff = _.assign({}, state.initData, dataDiff);
|
||||
|
||||
// Remove internal '__changeId' attribute.
|
||||
delete dataDiff.__changeId;
|
||||
// In case of 'non-edit' mode, changes are always there.
|
||||
state.changes = dataDiff;
|
||||
} else if (hasDataChanged) {
|
||||
const idAttr = schema.idAttribute;
|
||||
const idVal = state.initData[idAttr];
|
||||
// Append 'idAttr' only if it actually exists
|
||||
if (idVal) dataDiff[idAttr] = idVal;
|
||||
state.changes = dataDiff;
|
||||
} else {
|
||||
state.changes = null;
|
||||
}
|
||||
|
||||
state.data = sessData;
|
||||
|
||||
state.onDataChange(hasDataChanged, dataDiff);
|
||||
}
|
||||
|
||||
get isNew() {
|
||||
return this.schema.isNew(this.initData);
|
||||
}
|
||||
|
||||
set isNew(val) {
|
||||
throw new Error('Property \'isNew\' is readonly.', val);
|
||||
}
|
||||
|
||||
get isDirty() {
|
||||
return this.hasChanges;
|
||||
}
|
||||
|
||||
set isDirty(val) {
|
||||
throw new Error('Property \'isDirty\' is readonly.', val);
|
||||
}
|
||||
}
|
||||
|
||||
export const useSchemaState = ({
|
||||
schema, getInitData, immutableData, mode, keepCid, onDataChange,
|
||||
}) => {
|
||||
let schemaState = schema.state;
|
||||
|
||||
if (!schemaState) {
|
||||
schemaState = new SchemaState(
|
||||
schema, getInitData, immutableData, mode, keepCid, onDataChange
|
||||
);
|
||||
schema.state = schemaState;
|
||||
}
|
||||
|
||||
const [sessData, sessDispatch] = useReducer(
|
||||
sessDataReducer, {...(_.cloneDeep(schemaState.data)), __changeId: 0}
|
||||
);
|
||||
|
||||
const sessDispatchWithListener = (action) => {
|
||||
let dispatchPayload = {
|
||||
...action,
|
||||
depChange: (...args) => schemaState.getDepChange(...args),
|
||||
deferredDepChange: (...args) => schemaState.getDeferredDepChange(...args),
|
||||
};
|
||||
/*
|
||||
* All the session changes coming before init should be queued up.
|
||||
* They will be processed later when form is ready.
|
||||
*/
|
||||
let preReadyQueue = schemaState.preReadyQueue;
|
||||
|
||||
preReadyQueue ?
|
||||
preReadyQueue.push(dispatchPayload) :
|
||||
sessDispatch(dispatchPayload);
|
||||
};
|
||||
|
||||
schemaState.setUnpreparedData = (path, value) => {
|
||||
if(path) {
|
||||
let data = prepareData(value);
|
||||
_.set(schema.initData, path, data);
|
||||
sessDispatchWithListener({
|
||||
type: SCHEMA_STATE_ACTIONS.SET_VALUE,
|
||||
path: path,
|
||||
value: data,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const resetData = () => {
|
||||
const initData = _.cloneDeep(schemaState.initData);
|
||||
initData.__changeId = sessData.__changeId;
|
||||
sessDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.INIT,
|
||||
payload: initData,
|
||||
});
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
schemaState.initialise(sessDispatch, true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
schemaState.initialise(sessDispatch);
|
||||
}, [schemaState.loadingState]);
|
||||
|
||||
useEffect(() => {
|
||||
let preReadyQueue = schemaState.preReadyQueue;
|
||||
|
||||
if (!schemaState.isReady || !preReadyQueue) return;
|
||||
|
||||
for (const payload of preReadyQueue) {
|
||||
sessDispatch(payload);
|
||||
}
|
||||
|
||||
// Destroy the queue so that no one uses it.
|
||||
schemaState.preReadyQueue = null;
|
||||
}, [schemaState.isReady]);
|
||||
|
||||
useEffect(() => {
|
||||
// Validate the schema on the change of the data.
|
||||
schemaState.validate(sessData);
|
||||
}, [schemaState.isReady, sessData.__changeId]);
|
||||
|
||||
useEffect(() => {
|
||||
const items = sessData.__deferred__ || [];
|
||||
|
||||
if (items.length == 0) return;
|
||||
|
||||
sessDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.CLEAR_DEFERRED_QUEUE,
|
||||
});
|
||||
|
||||
items.forEach((item) => {
|
||||
item.promise.then((resFunc) => {
|
||||
sessDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.DEFERRED_DEPCHANGE,
|
||||
path: item.action.path,
|
||||
depChange: item.action.depChange,
|
||||
listener: {
|
||||
...item.listener,
|
||||
callback: resFunc,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}, [schemaState.__deferred__?.length]);
|
||||
|
||||
schemaState.reload = reload;
|
||||
schemaState.reset = resetData;
|
||||
|
||||
return {
|
||||
schemaState,
|
||||
dataDispatch: sessDispatchWithListener,
|
||||
sessData,
|
||||
reset: resetData,
|
||||
};
|
||||
};
|
|
@ -122,7 +122,10 @@ class Notifier {
|
|||
|
||||
pgRespErrorNotify(error, prefixMsg='') {
|
||||
if (error.response?.status === 410) {
|
||||
this.alert(gettext('Error: Object not found - %s.', error.response.statusText), parseApiError(error));
|
||||
this.alert(
|
||||
gettext('Error: Object not found - %s.', error.response.statusText),
|
||||
parseApiError(error)
|
||||
);
|
||||
} else {
|
||||
this.error(prefixMsg + ' ' + parseApiError(error));
|
||||
}
|
||||
|
@ -163,7 +166,7 @@ class Notifier {
|
|||
return onJSONResult();
|
||||
}
|
||||
this.alert(promptmsg, msg.replace(new RegExp(/\r?\n/, 'g'), '<br />'));
|
||||
onJSONResult('ALERT_CALLED');
|
||||
onJSONResult?.('ALERT_CALLED');
|
||||
}
|
||||
|
||||
alert(title, text, onOkClick, okLabel=gettext('OK')) {
|
||||
|
|
|
@ -304,9 +304,9 @@ class PGUtilitiesBackupFeatureTest(BaseFeatureTest):
|
|||
existing_path = path_input.get_property("value")
|
||||
if existing_path != default_binary_path[serv]:
|
||||
path_already_set = False
|
||||
self.page.clear_edit_box(path_input)
|
||||
path_input.click()
|
||||
path_input.send_keys(default_binary_path[serv])
|
||||
self.page.fill_input(
|
||||
path_input, default_binary_path[serv]
|
||||
)
|
||||
else:
|
||||
print('Binary path Key is Incorrect')
|
||||
else:
|
||||
|
|
|
@ -851,23 +851,25 @@ class PgadminPage:
|
|||
self.driver.execute_script(
|
||||
"arguments[0].dispatchEvent(new Event('blur'));", field)
|
||||
|
||||
def fill_input(self, field, field_content, input_keys=False,
|
||||
key_after_input=Keys.ARROW_DOWN):
|
||||
try:
|
||||
attempt = 0
|
||||
def fill_input(
|
||||
self, field, field_content, input_keys=False,
|
||||
key_after_input=Keys.ARROW_DOWN
|
||||
):
|
||||
for attempt in range(0, 3):
|
||||
try:
|
||||
field.click()
|
||||
break
|
||||
except Exception as e:
|
||||
time.sleep(.2)
|
||||
if attempt == 2:
|
||||
raise e
|
||||
|
||||
# Use send keys if input_keys true, else use javascript to set content
|
||||
if input_keys:
|
||||
backspaces = [Keys.BACKSPACE] * len(field.get_attribute('value'))
|
||||
field.send_keys(backspaces)
|
||||
field.send_keys(str(field_content))
|
||||
# self.wait_for_input_by_element(field, field_content)
|
||||
# Clear the existing content first
|
||||
self.clear_edit_box(field)
|
||||
# Send the keys one by one.
|
||||
[field.send_keys(c) for c in str(field_content)]
|
||||
else:
|
||||
self.driver.execute_script("arguments[0].value = arguments[1]",
|
||||
field, field_content)
|
||||
|
|
|
@ -110,8 +110,8 @@ describe('SchemaView', ()=>{
|
|||
});
|
||||
|
||||
it('onReset after change', async ()=>{
|
||||
onDataChange.mockClear();
|
||||
await simulateChanges();
|
||||
onDataChange.mockClear();
|
||||
let confirmSpy = jest.spyOn(pgAdmin.Browser.notifier, 'confirm');
|
||||
await user.click(ctrl.container.querySelector('[data-test="Reset"]'));
|
||||
/* Press OK */
|
||||
|
|
Loading…
Reference in New Issue