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",
|
"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-html": "yarn audit --json | yarn run yarn-audit-html --output ../auditjs.html",
|
||||||
"auditjs": "yarn audit --groups dependencies",
|
"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"
|
"audit-all": "yarn run auditjs && yarn run auditpy"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@3.8.3",
|
"packageManager": "yarn@3.8.3",
|
||||||
|
|
|
@ -42,7 +42,18 @@ export default function ObjectNodeProperties({panelId, node, treeNodeInfo, nodeD
|
||||||
let warnOnCloseFlag = true;
|
let warnOnCloseFlag = true;
|
||||||
const confirmOnCloseReset = usePreferences().getPreferencesForModule('browser').confirm_on_properties_close;
|
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 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,
|
// We only have two actionTypes, 'create' and 'edit' to initiate the dialog,
|
||||||
// so if isActionTypeCopy is true, we should revert back to "create" since
|
// 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) {
|
function savePreferences(data, initVal) {
|
||||||
let _data = [];
|
let _data = [];
|
||||||
for (const [key, value] of Object.entries(data.current)) {
|
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) {
|
if (_metadata.length > 0) {
|
||||||
let val = getCollectionValue(_metadata, value, initVal);
|
let val = getCollectionValue(_metadata, value, initVal);
|
||||||
_data.push({
|
_data.push({
|
||||||
|
@ -525,7 +527,11 @@ export default function PreferencesComponent({ ...props }) {
|
||||||
data: save_data,
|
data: save_data,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
let requiresTreeRefresh = save_data.some((s)=>{
|
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;
|
let requires_refresh = false;
|
||||||
for (const [key] of Object.entries(data.current)) {
|
for (const [key] of Object.entries(data.current)) {
|
||||||
|
@ -536,11 +542,15 @@ export default function PreferencesComponent({ ...props }) {
|
||||||
if (requiresTreeRefresh) {
|
if (requiresTreeRefresh) {
|
||||||
pgAdmin.Browser.notifier.confirm(
|
pgAdmin.Browser.notifier.confirm(
|
||||||
gettext('Object explorer refresh required'),
|
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 () {
|
function () {
|
||||||
pgAdmin.Browser.tree.destroy().then(
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,12 +10,8 @@
|
||||||
/* The DataGridView component is based on react-table component */
|
/* The DataGridView component is based on react-table component */
|
||||||
|
|
||||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { styled } from '@mui/material/styles';
|
|
||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import { PgIconButton } from '../components/Buttons';
|
|
||||||
import AddIcon from '@mui/icons-material/AddOutlined';
|
import AddIcon from '@mui/icons-material/AddOutlined';
|
||||||
import { MappedCellControl } from './MappedControl';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useReactTable,
|
useReactTable,
|
||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
|
@ -24,103 +20,41 @@ import {
|
||||||
getExpandedRowModel,
|
getExpandedRowModel,
|
||||||
flexRender,
|
flexRender,
|
||||||
} from '@tanstack/react-table';
|
} from '@tanstack/react-table';
|
||||||
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
||||||
import {HTML5Backend} from 'react-dnd-html5-backend';
|
import {HTML5Backend} from 'react-dnd-html5-backend';
|
||||||
|
|
||||||
import gettext from 'sources/gettext';
|
import { usePgAdmin } from 'sources/BrowserComponent';
|
||||||
import { SCHEMA_STATE_ACTIONS, StateUtilsContext } from '.';
|
import { PgIconButton } from 'sources/components/Buttons';
|
||||||
import FormView, { getFieldMetaData } from './FormView';
|
import {
|
||||||
import CustomPropTypes from 'sources/custom_prop_types';
|
PgReactTable, PgReactTableBody, PgReactTableCell, PgReactTableHeader,
|
||||||
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,
|
|
||||||
PgReactTableRow, PgReactTableRowContent, PgReactTableRowExpandContent,
|
PgReactTableRow, PgReactTableRowContent, PgReactTableRowExpandContent,
|
||||||
getDeleteCell, getEditCell, getReorderCell } from '../components/PgReactTableStyled';
|
getDeleteCell, getEditCell, getReorderCell
|
||||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
} 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}) => ({
|
import FormView from './FormView';
|
||||||
'& .DataGridView-grid': {
|
import { MappedCellControl } from './MappedControl';
|
||||||
...theme.mixins.panelBorder,
|
import {
|
||||||
backgroundColor: theme.palette.background.default,
|
SCHEMA_STATE_ACTIONS, SchemaStateContext, getFieldMetaData,
|
||||||
display: 'flex',
|
isModeSupportedByField
|
||||||
flexDirection: 'column',
|
} from './common';
|
||||||
minHeight: 0,
|
import { StyleDataGridBox } from './StyledComponents';
|
||||||
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',
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
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 [key, setKey] = useState(false);
|
||||||
const depListener = useContext(DepListenerContext);
|
const schemaState = useContext(SchemaStateContext);
|
||||||
const rowRef = useRef(null);
|
const rowRef = useRef(null);
|
||||||
const dragHandleRef = useRef(null);
|
const dragHandleRef = useRef(null);
|
||||||
|
|
||||||
|
@ -150,7 +84,7 @@ function DataTableRow({index, row, totalRows, isResizing, isHovered, schema, sch
|
||||||
schemaRef.current.fields.forEach((field)=>{
|
schemaRef.current.fields.forEach((field)=>{
|
||||||
/* Self change is also dep change */
|
/* Self change is also dep change */
|
||||||
if(field.depChange || field.deferredDepChange) {
|
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)=>{
|
(evalFunc(null, field.deps) || []).forEach((dep)=>{
|
||||||
let source = accessPath.concat(dep);
|
let source = accessPath.concat(dep);
|
||||||
|
@ -158,14 +92,14 @@ function DataTableRow({index, row, totalRows, isResizing, isHovered, schema, sch
|
||||||
source = dep;
|
source = dep;
|
||||||
}
|
}
|
||||||
if(field.depChange) {
|
if(field.depChange) {
|
||||||
depListener?.addDepListener(source, accessPath.concat(field.id), field.depChange);
|
schemaState?.addDepListener(source, accessPath.concat(field.id), field.depChange);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return ()=>{
|
return ()=>{
|
||||||
/* Cleanup the listeners when unmounting */
|
/* 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);
|
drop(rowRef);
|
||||||
|
|
||||||
return useMemo(()=>
|
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) => {
|
{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, {
|
const content = flexRender(cell.column.columnDef.cell, {
|
||||||
key: cell.column.columnDef.cell?.type ?? cell.column.columnDef.id,
|
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);}
|
reRenderRow: ()=>{setKey((currKey)=>!currKey);}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (modeSupported &&
|
return (
|
||||||
<PgReactTableCell cell={cell} row={row} key={cell.id} ref={cell.column.id == 'btn-reorder' ? dragHandleRef : null}>
|
<PgReactTableCell cell={cell} row={row} key={cell.id}
|
||||||
|
ref={cell.column.id == 'btn-reorder' ? dragHandleRef : null}>
|
||||||
{content}
|
{content}
|
||||||
</PgReactTableCell>
|
</PgReactTableCell>
|
||||||
);
|
);
|
||||||
|
@ -337,9 +277,10 @@ function getMappedCell({
|
||||||
|
|
||||||
export default function DataGridView({
|
export default function DataGridView({
|
||||||
value, viewHelperProps, schema, accessPath, dataDispatch, containerClassName,
|
value, viewHelperProps, schema, accessPath, dataDispatch, containerClassName,
|
||||||
fixedRows, ...props}) {
|
fixedRows, ...props
|
||||||
|
}) {
|
||||||
|
|
||||||
const stateUtils = useContext(StateUtilsContext);
|
const schemaState = useContext(SchemaStateContext);
|
||||||
const checkIsMounted = useIsMounted();
|
const checkIsMounted = useIsMounted();
|
||||||
const [hoverIndex, setHoverIndex] = useState();
|
const [hoverIndex, setHoverIndex] = useState();
|
||||||
const newRowIndex = useRef();
|
const newRowIndex = useRef();
|
||||||
|
@ -439,14 +380,14 @@ export default function DataGridView({
|
||||||
}
|
}
|
||||||
|
|
||||||
cols = cols.concat(
|
cols = cols.concat(
|
||||||
schemaRef.current.fields.filter((f)=>{
|
schemaRef.current.fields.filter((f) => (
|
||||||
return _.isArray(props.columns) ? props.columns.indexOf(f.id) > -1 : true;
|
_.isArray(props.columns) ? props.columns.indexOf(f.id) > -1 : true
|
||||||
}).sort((firstF, secondF)=>{
|
)).sort((firstF, secondF) => (
|
||||||
if(_.isArray(props.columns)) {
|
_.isArray(props.columns) ? ((
|
||||||
return props.columns.indexOf(firstF.id) < props.columns.indexOf(secondF.id) ? -1 : 1;
|
props.columns.indexOf(firstF.id) <
|
||||||
}
|
props.columns.indexOf(secondF.id)
|
||||||
return 0;
|
) ? -1 : 1) : 0
|
||||||
}).map((field)=>{
|
)).map((field) => {
|
||||||
let widthParms = {};
|
let widthParms = {};
|
||||||
if(field.width) {
|
if(field.width) {
|
||||||
widthParms.size = field.width;
|
widthParms.size = field.width;
|
||||||
|
@ -461,7 +402,10 @@ export default function DataGridView({
|
||||||
if(field.maxWidth) {
|
if(field.maxWidth) {
|
||||||
widthParms.maxSize = 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 = {
|
let colInfo = {
|
||||||
header: field.label||<> </>,
|
header: field.label||<> </>,
|
||||||
|
@ -490,8 +434,7 @@ export default function DataGridView({
|
||||||
const ret = {};
|
const ret = {};
|
||||||
|
|
||||||
columns.forEach(column => {
|
columns.forEach(column => {
|
||||||
let {modeSupported} = column.field ? getFieldMetaData(column.field, schemaRef.current, {}, viewHelperProps) : {modeSupported: true};
|
ret[column.id] = isModeSupportedByField(column.field, viewHelperProps);
|
||||||
ret[column.id] = modeSupported;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
|
@ -535,19 +478,20 @@ export default function DataGridView({
|
||||||
});
|
});
|
||||||
}, [props.canAddRow, rows?.length]);
|
}, [props.canAddRow, rows?.length]);
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(() => {
|
||||||
let rowsPromise = fixedRows;
|
let rowsPromise = fixedRows;
|
||||||
|
|
||||||
/* If fixedRows is defined, fetch the details */
|
// If fixedRows is defined, fetch the details.
|
||||||
if(typeof rowsPromise === 'function') {
|
if(typeof rowsPromise === 'function') {
|
||||||
rowsPromise = rowsPromise();
|
rowsPromise = rowsPromise();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(rowsPromise) {
|
if(rowsPromise) {
|
||||||
Promise.resolve(rowsPromise)
|
Promise.resolve(rowsPromise)
|
||||||
.then((res)=>{
|
.then((res) => {
|
||||||
/* If component unmounted, dont update state */
|
/* If component unmounted, dont update state */
|
||||||
if(checkIsMounted()) {
|
if(checkIsMounted()) {
|
||||||
stateUtils.initOrigData(accessPath, res);
|
schemaState.setUnpreparedData(accessPath, res);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -558,12 +502,17 @@ export default function DataGridView({
|
||||||
virtualizer.scrollToIndex(newRowIndex.current);
|
virtualizer.scrollToIndex(newRowIndex.current);
|
||||||
|
|
||||||
// Try autofocus on newly added row.
|
// Try autofocus on newly added row.
|
||||||
setTimeout(()=>{
|
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;
|
if(!rowInput) return;
|
||||||
|
|
||||||
requestAnimationAndFocus(tableRef.current.querySelector(`.pgrt-row[data-index="${newRowIndex.current}"] input`));
|
requestAnimationAndFocus(tableRef.current.querySelector(
|
||||||
props.expandEditOnAdd && props.canEdit && rows[newRowIndex.current]?.toggleExpanded(true);
|
`.pgrt-row[data-index="${newRowIndex.current}"] input`
|
||||||
|
));
|
||||||
|
props.expandEditOnAdd && props.canEdit &&
|
||||||
|
rows[newRowIndex.current]?.toggleExpanded(true);
|
||||||
newRowIndex.current = undefined;
|
newRowIndex.current = undefined;
|
||||||
}, 50);
|
}, 50);
|
||||||
}
|
}
|
||||||
|
@ -599,7 +548,7 @@ export default function DataGridView({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledBox className={containerClassName}>
|
<StyleDataGridBox className={containerClassName}>
|
||||||
<Box className='DataGridView-grid'>
|
<Box className='DataGridView-grid'>
|
||||||
{(props.label || props.canAdd) && <DataGridHeader label={props.label} canAdd={props.canAdd} onAddClick={onAddClick}
|
{(props.label || props.canAdd) && <DataGridHeader label={props.label} canAdd={props.canAdd} onAddClick={onAddClick}
|
||||||
canSearch={props.canSearch}
|
canSearch={props.canSearch}
|
||||||
|
@ -639,7 +588,7 @@ export default function DataGridView({
|
||||||
</PgReactTable>
|
</PgReactTable>
|
||||||
</DndProvider>
|
</DndProvider>
|
||||||
</Box>
|
</Box>
|
||||||
</StyledBox>
|
</StyleDataGridBox>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,11 +7,9 @@
|
||||||
//
|
//
|
||||||
//////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export const DepListenerContext = React.createContext();
|
|
||||||
|
|
||||||
export default class DepListener {
|
export class DepListener {
|
||||||
constructor() {
|
constructor() {
|
||||||
this._depListeners = [];
|
this._depListeners = [];
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,38 +8,49 @@
|
||||||
//////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
import React, { useContext, useEffect } from 'react';
|
import React, { useContext, useEffect } from 'react';
|
||||||
|
|
||||||
|
import Grid from '@mui/material/Grid';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { MappedFormControl } from './MappedControl';
|
import FieldSet from 'sources/components/FieldSet';
|
||||||
import { SCHEMA_STATE_ACTIONS, StateUtilsContext } from '.';
|
import CustomPropTypes from 'sources/custom_prop_types';
|
||||||
import { evalFunc } from 'sources/utils';
|
import { evalFunc } from 'sources/utils';
|
||||||
import CustomPropTypes from '../custom_prop_types';
|
|
||||||
import { DepListenerContext } from './DepListener';
|
import { MappedFormControl } from './MappedControl';
|
||||||
import { getFieldMetaData } from './FormView';
|
import {
|
||||||
import FieldSet from '../components/FieldSet';
|
getFieldMetaData, SCHEMA_STATE_ACTIONS, SchemaStateContext
|
||||||
import { Grid } from '@mui/material';
|
} from './common';
|
||||||
|
|
||||||
|
|
||||||
|
const INLINE_COMPONENT_ROWGAP = '8px';
|
||||||
|
|
||||||
export default function FieldSetView({
|
export default function FieldSetView({
|
||||||
value, schema={}, viewHelperProps, accessPath, dataDispatch, controlClassName, isDataGridForm=false, label, visible}) {
|
value, schema={}, viewHelperProps, accessPath, dataDispatch,
|
||||||
const depListener = useContext(DepListenerContext);
|
controlClassName, isDataGridForm=false, label, visible
|
||||||
const stateUtils = useContext(StateUtilsContext);
|
}) {
|
||||||
|
const schemaState = useContext(SchemaStateContext);
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(() => {
|
||||||
/* Calculate the fields which depends on the current field */
|
// Calculate the fields which depends on the current field.
|
||||||
if(!isDataGridForm && depListener) {
|
if(!isDataGridForm && schemaState) {
|
||||||
schema.fields.forEach((field)=>{
|
schema.fields.forEach((field) => {
|
||||||
/* Self change is also dep change */
|
/* Self change is also dep change */
|
||||||
if(field.depChange || field.deferredDepChange) {
|
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)=>{
|
(evalFunc(null, field.deps) || []).forEach((dep) => {
|
||||||
let source = accessPath.concat(dep);
|
let source = accessPath.concat(dep);
|
||||||
if(_.isArray(dep)) {
|
if(_.isArray(dep)) {
|
||||||
source = dep;
|
source = dep;
|
||||||
}
|
}
|
||||||
if(field.depChange) {
|
if(field.depChange) {
|
||||||
depListener.addDepListener(source, accessPath.concat(field.id), field.depChange);
|
schemaState?.addDepListener(
|
||||||
|
source, accessPath.concat(field.id), field.depChange
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -49,80 +60,86 @@ export default function FieldSetView({
|
||||||
let viewFields = [];
|
let viewFields = [];
|
||||||
let inlineComponents = [];
|
let inlineComponents = [];
|
||||||
|
|
||||||
/* Prepare the array of components based on the types */
|
if(!visible) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare the array of components based on the types.
|
||||||
for(const field of schema.fields) {
|
for(const field of schema.fields) {
|
||||||
let {visible, disabled, readonly, modeSupported} =
|
const {
|
||||||
getFieldMetaData(field, schema, value, viewHelperProps);
|
visible, disabled, readonly, modeSupported
|
||||||
|
} = getFieldMetaData(field, schema, value, viewHelperProps);
|
||||||
|
|
||||||
if(modeSupported) {
|
if(!modeSupported) continue;
|
||||||
/* 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.
|
|
||||||
*/
|
|
||||||
const currentControl = <MappedFormControl
|
|
||||||
state={value}
|
|
||||||
key={field.id}
|
|
||||||
viewHelperProps={viewHelperProps}
|
|
||||||
name={field.id}
|
|
||||||
value={value[field.id]}
|
|
||||||
{...field}
|
|
||||||
readonly={readonly}
|
|
||||||
disabled={disabled}
|
|
||||||
visible={visible}
|
|
||||||
onChange={(changeValue)=>{
|
|
||||||
/* Get the changes on dependent fields as well */
|
|
||||||
dataDispatch({
|
|
||||||
type: SCHEMA_STATE_ACTIONS.SET_VALUE,
|
|
||||||
path: accessPath.concat(field.id),
|
|
||||||
value: changeValue,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
hasError={hasError}
|
|
||||||
className={controlClassName}
|
|
||||||
memoDeps={[
|
|
||||||
value[field.id],
|
|
||||||
readonly,
|
|
||||||
disabled,
|
|
||||||
visible,
|
|
||||||
hasError,
|
|
||||||
controlClassName,
|
|
||||||
...(evalFunc(null, field.deps) || []).map((dep)=>value[dep]),
|
|
||||||
]}
|
|
||||||
/>;
|
|
||||||
|
|
||||||
if(field.inlineNext) {
|
// Its a form control.
|
||||||
inlineComponents.push(React.cloneElement(currentControl, {
|
const hasError = (field.id === schemaState?.errors.name);
|
||||||
withContainer: false, controlGridBasis: 3
|
|
||||||
}));
|
/*
|
||||||
} else if(inlineComponents?.length > 0) {
|
* When there is a change, the dependent values can also change.
|
||||||
inlineComponents.push(React.cloneElement(currentControl, {
|
* Let's pass these changes to dependent for take them into effect to
|
||||||
withContainer: false, controlGridBasis: 3
|
* generate new values.
|
||||||
}));
|
*/
|
||||||
viewFields.push(
|
const currentControl = <MappedFormControl
|
||||||
<Grid container spacing={0} key={`ic-${inlineComponents[0].key}`} className={controlClassName} rowGap="8px">
|
state={value}
|
||||||
{inlineComponents}
|
key={field.id}
|
||||||
</Grid>
|
viewHelperProps={viewHelperProps}
|
||||||
);
|
name={field.id}
|
||||||
inlineComponents = [];
|
value={value[field.id]}
|
||||||
} else {
|
{...field}
|
||||||
viewFields.push(currentControl);
|
readonly={readonly}
|
||||||
}
|
disabled={disabled}
|
||||||
|
visible={visible}
|
||||||
|
onChange={(changeValue)=>{
|
||||||
|
/* Get the changes on dependent fields as well */
|
||||||
|
dataDispatch({
|
||||||
|
type: SCHEMA_STATE_ACTIONS.SET_VALUE,
|
||||||
|
path: accessPath.concat(field.id),
|
||||||
|
value: changeValue,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
hasError={hasError}
|
||||||
|
className={controlClassName}
|
||||||
|
memoDeps={[
|
||||||
|
value[field.id],
|
||||||
|
readonly,
|
||||||
|
disabled,
|
||||||
|
visible,
|
||||||
|
hasError,
|
||||||
|
controlClassName,
|
||||||
|
...(evalFunc(null, field.deps) || []).map((dep)=>value[dep]),
|
||||||
|
]}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
if(field.inlineNext) {
|
||||||
|
inlineComponents.push(React.cloneElement(currentControl, {
|
||||||
|
withContainer: false, controlGridBasis: 3
|
||||||
|
}));
|
||||||
|
} else if(inlineComponents?.length > 0) {
|
||||||
|
inlineComponents.push(React.cloneElement(currentControl, {
|
||||||
|
withContainer: false, controlGridBasis: 3
|
||||||
|
}));
|
||||||
|
viewFields.push(
|
||||||
|
<Grid container spacing={0} key={`ic-${inlineComponents[0].key}`}
|
||||||
|
className={controlClassName} rowGap={INLINE_COMPONENT_ROWGAP}>
|
||||||
|
{inlineComponents}
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
inlineComponents = [];
|
||||||
|
} else {
|
||||||
|
viewFields.push(currentControl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(inlineComponents?.length > 0) {
|
if(inlineComponents?.length > 0) {
|
||||||
viewFields.push(
|
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}
|
{inlineComponents}
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!visible) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FieldSet title={label} className={controlClassName}>
|
<FieldSet title={label} className={controlClassName}>
|
||||||
{viewFields}
|
{viewFields}
|
||||||
|
|
|
@ -7,69 +7,29 @@
|
||||||
//
|
//
|
||||||
//////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
import React, {
|
||||||
import { styled } from '@mui/material/styles';
|
useContext, useEffect, useMemo, useRef, useState
|
||||||
|
} from 'react';
|
||||||
import { Box, Tab, Tabs, Grid } from '@mui/material';
|
import { Box, Tab, Tabs, Grid } from '@mui/material';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { MappedFormControl } from './MappedControl';
|
import { FormNote, InputSQL } from 'sources/components/FormComponents';
|
||||||
import TabPanel from '../components/TabPanel';
|
import TabPanel from 'sources/components/TabPanel';
|
||||||
import DataGridView from './DataGridView';
|
import { useOnScreen } from 'sources/custom_hooks';
|
||||||
import { SCHEMA_STATE_ACTIONS, StateUtilsContext } from '.';
|
import CustomPropTypes from 'sources/custom_prop_types';
|
||||||
import { FormNote, InputSQL } from '../components/FormComponents';
|
|
||||||
import gettext from 'sources/gettext';
|
import gettext from 'sources/gettext';
|
||||||
import { evalFunc } from 'sources/utils';
|
import { evalFunc } from 'sources/utils';
|
||||||
import CustomPropTypes from '../custom_prop_types';
|
|
||||||
import { useOnScreen } from '../custom_hooks';
|
|
||||||
import { DepListenerContext } from './DepListener';
|
|
||||||
import FieldSetView from './FieldSetView';
|
|
||||||
|
|
||||||
const StyledBox = styled(Box)(({theme}) => ({
|
import DataGridView from './DataGridView';
|
||||||
'& .FormView-nestedControl': {
|
import { MappedFormControl } from './MappedControl';
|
||||||
height: 'unset !important',
|
import FieldSetView from './FieldSetView';
|
||||||
'& .FormView-controlRow': {
|
import {
|
||||||
marginBottom: theme.spacing(1),
|
SCHEMA_STATE_ACTIONS, SchemaStateContext, getFieldMetaData
|
||||||
},
|
} from './common';
|
||||||
'& .FormView-nestedTabPanel': {
|
|
||||||
backgroundColor: theme.otherVars.headerBg,
|
import { FormContentBox } from './StyledComponents';
|
||||||
}
|
|
||||||
},
|
|
||||||
'& .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%',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
/* Optional SQL tab */
|
/* Optional SQL tab */
|
||||||
function SQLTab({active, getSQLValue}) {
|
function SQLTab({active, getSQLValue}) {
|
||||||
|
@ -102,77 +62,12 @@ SQLTab.propTypes = {
|
||||||
getSQLValue: PropTypes.func.isRequired,
|
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 */
|
/* The first component of schema view form */
|
||||||
export default function FormView({
|
export default function FormView({
|
||||||
value, schema={}, viewHelperProps, isNested=false, accessPath, dataDispatch, hasSQLTab,
|
value, schema={}, viewHelperProps, isNested=false, accessPath, dataDispatch, hasSQLTab,
|
||||||
getSQLValue, onTabChange, firstEleRef, className, isDataGridForm=false, isTabView=true, visible}) {
|
getSQLValue, onTabChange, firstEleRef, className, isDataGridForm=false, isTabView=true, visible}) {
|
||||||
let defaultTab = 'General';
|
let defaultTab = gettext('General');
|
||||||
let tabs = {};
|
let tabs = {};
|
||||||
let tabsClassname = {};
|
let tabsClassname = {};
|
||||||
const [tabValue, setTabValue] = useState(0);
|
const [tabValue, setTabValue] = useState(0);
|
||||||
|
@ -180,10 +75,9 @@ export default function FormView({
|
||||||
const firstEleID = useRef();
|
const firstEleID = useRef();
|
||||||
const formRef = useRef();
|
const formRef = useRef();
|
||||||
const onScreenTracker = useRef(false);
|
const onScreenTracker = useRef(false);
|
||||||
const depListener = useContext(DepListenerContext);
|
|
||||||
let groupLabels = {};
|
let groupLabels = {};
|
||||||
const schemaRef = useRef(schema);
|
const schemaRef = useRef(schema);
|
||||||
const stateUtils = useContext(StateUtilsContext);
|
const schemaState = useContext(SchemaStateContext);
|
||||||
|
|
||||||
let isOnScreen = useOnScreen(formRef);
|
let isOnScreen = useOnScreen(formRef);
|
||||||
|
|
||||||
|
@ -206,7 +100,7 @@ export default function FormView({
|
||||||
schemaRef.current.fields.forEach((field)=>{
|
schemaRef.current.fields.forEach((field)=>{
|
||||||
/* Self change is also dep change */
|
/* Self change is also dep change */
|
||||||
if(field.depChange || field.deferredDepChange) {
|
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)=>{
|
(evalFunc(null, field.deps) || []).forEach((dep)=>{
|
||||||
// when dep is a string then prepend the complete accessPath
|
// when dep is a string then prepend the complete accessPath
|
||||||
|
@ -217,24 +111,25 @@ export default function FormView({
|
||||||
source = dep;
|
source = dep;
|
||||||
}
|
}
|
||||||
if(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);
|
||||||
}
|
}
|
||||||
if(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 ()=>{
|
return ()=>{
|
||||||
/* Cleanup the listeners when unmounting */
|
/* Cleanup the listeners when unmounting */
|
||||||
depListener.removeDepListener(accessPath);
|
schemaState?.removeDepListener(accessPath);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/* Upon reset, set the tab to first */
|
/* Upon reset, set the tab to first */
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
setTabValue(0);
|
if (schemaState?.isReady)
|
||||||
}, [stateUtils.formResetKey]);
|
setTabValue(0);
|
||||||
|
}, [schemaState?.isReady]);
|
||||||
|
|
||||||
let fullTabs = [];
|
let fullTabs = [];
|
||||||
let inlineComponents = [];
|
let inlineComponents = [];
|
||||||
|
@ -242,185 +137,197 @@ export default function FormView({
|
||||||
|
|
||||||
/* Prepare the array of components based on the types */
|
/* Prepare the array of components based on the types */
|
||||||
for(const field of schemaRef.current.fields) {
|
for(const field of schemaRef.current.fields) {
|
||||||
let {visible, disabled, readonly, canAdd, canEdit, canDelete, canReorder, canAddRow, modeSupported} =
|
let {
|
||||||
getFieldMetaData(field, schema, value, viewHelperProps);
|
visible, disabled, readonly, canAdd, canEdit, canDelete, canReorder,
|
||||||
|
canAddRow, modeSupported
|
||||||
|
} = getFieldMetaData(field, schema, value, viewHelperProps);
|
||||||
|
|
||||||
if(modeSupported) {
|
if(!modeSupported) continue;
|
||||||
let {group, CustomControl} = field;
|
|
||||||
if(field.type === 'group') {
|
let {group, CustomControl} = field;
|
||||||
groupLabels[field.id] = field.label;
|
|
||||||
if(!visible) {
|
if(field.type === 'group') {
|
||||||
schemaRef.current.filterGroups.push(field.label);
|
groupLabels[field.id] = field.label;
|
||||||
}
|
|
||||||
continue;
|
if(!visible) {
|
||||||
|
schemaRef.current.filterGroups.push(field.label);
|
||||||
}
|
}
|
||||||
group = groupLabels[group] || group || defaultTab;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if(!tabs[group]) tabs[group] = [];
|
group = groupLabels[group] || group || defaultTab;
|
||||||
|
|
||||||
/* Lets choose the path based on type */
|
if(!tabs[group]) tabs[group] = [];
|
||||||
if(field.type === 'nested-tab') {
|
|
||||||
/* Pass on the top schema */
|
|
||||||
if(isNested) {
|
|
||||||
field.schema.top = schemaRef.current.top;
|
|
||||||
} else {
|
|
||||||
field.schema.top = schema;
|
|
||||||
}
|
|
||||||
tabs[group].push(
|
|
||||||
<FormView key={`nested${tabs[group].length}`} value={value} viewHelperProps={viewHelperProps}
|
|
||||||
schema={field.schema} accessPath={accessPath} dataDispatch={dataDispatch} isNested={true} isDataGridForm={isDataGridForm}
|
|
||||||
{...field} visible={visible}/>
|
|
||||||
);
|
|
||||||
} else if(field.type === 'nested-fieldset') {
|
|
||||||
/* Pass on the top schema */
|
|
||||||
if(isNested) {
|
|
||||||
field.schema.top = schemaRef.current.top;
|
|
||||||
} else {
|
|
||||||
field.schema.top = schema;
|
|
||||||
}
|
|
||||||
tabs[group].push(
|
|
||||||
<FieldSetView key={`nested${tabs[group].length}`} value={value} viewHelperProps={viewHelperProps}
|
|
||||||
schema={field.schema} accessPath={accessPath} dataDispatch={dataDispatch} isNested={true} isDataGridForm={isDataGridForm}
|
|
||||||
controlClassName='FormView-controlRow'
|
|
||||||
{...field} visible={visible}/>
|
|
||||||
);
|
|
||||||
} else if(field.type === 'collection') {
|
|
||||||
/* If its a collection, let data grid view handle it */
|
|
||||||
/* Pass on the top schema */
|
|
||||||
if(isNested) {
|
|
||||||
field.schema.top = schemaRef.current.top;
|
|
||||||
} else {
|
|
||||||
field.schema.top = schemaRef.current;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!_.isUndefined(field.fixedRows)) {
|
// Lets choose the path based on type.
|
||||||
canAdd = false;
|
if(field.type === 'nested-tab') {
|
||||||
canDelete = false;
|
/* Pass on the top schema */
|
||||||
}
|
if(isNested) {
|
||||||
|
field.schema.top = schemaRef.current.top;
|
||||||
const ctrlProps = {
|
|
||||||
key: field.id, ...field,
|
|
||||||
value: value[field.id] || [], viewHelperProps: viewHelperProps,
|
|
||||||
schema: field.schema, accessPath: accessPath.concat(field.id), dataDispatch: dataDispatch,
|
|
||||||
containerClassName: 'FormView-controlRow',
|
|
||||||
canAdd: canAdd, canReorder: canReorder,
|
|
||||||
canEdit: canEdit, canDelete: canDelete,
|
|
||||||
visible: visible, canAddRow: canAddRow, onDelete: field.onDelete, canSearch: field.canSearch,
|
|
||||||
expandEditOnAdd: field.expandEditOnAdd,
|
|
||||||
fixedRows: (viewHelperProps.mode == 'create' ? field.fixedRows : undefined),
|
|
||||||
addOnTop: Boolean(field.addOnTop)
|
|
||||||
};
|
|
||||||
|
|
||||||
if(CustomControl) {
|
|
||||||
tabs[group].push(<CustomControl {...ctrlProps}/>);
|
|
||||||
} else {
|
|
||||||
tabs[group].push(<DataGridView {...ctrlProps} />);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
/* Its a form control */
|
field.schema.top = schema;
|
||||||
const hasError = _.isEqual(accessPath.concat(field.id), stateUtils.formErr.name);
|
}
|
||||||
/* When there is a change, the dependent values can change
|
tabs[group].push(
|
||||||
* lets pass the new changes to dependent and get the new values
|
<FormView key={`nested${tabs[group].length}`} value={value} viewHelperProps={viewHelperProps}
|
||||||
* from there as well.
|
schema={field.schema} accessPath={accessPath} dataDispatch={dataDispatch} isNested={true} isDataGridForm={isDataGridForm}
|
||||||
*/
|
{...field} visible={visible}/>
|
||||||
if(field.isFullTab) {
|
);
|
||||||
tabsClassname[group] ='FormView-fullSpace';
|
} else if(field.type === 'nested-fieldset') {
|
||||||
fullTabs.push(group);
|
/* Pass on the top schema */
|
||||||
}
|
if(isNested) {
|
||||||
|
field.schema.top = schemaRef.current.top;
|
||||||
|
} else {
|
||||||
|
field.schema.top = schema;
|
||||||
|
}
|
||||||
|
tabs[group].push(
|
||||||
|
<FieldSetView key={`nested${tabs[group].length}`} value={value} viewHelperProps={viewHelperProps}
|
||||||
|
schema={field.schema} accessPath={accessPath} dataDispatch={dataDispatch} isNested={true} isDataGridForm={isDataGridForm}
|
||||||
|
controlClassName='FormView-controlRow'
|
||||||
|
{...field} visible={visible}/>
|
||||||
|
);
|
||||||
|
} else if(field.type === 'collection') {
|
||||||
|
/* If its a collection, let data grid view handle it */
|
||||||
|
/* Pass on the top schema */
|
||||||
|
if(isNested) {
|
||||||
|
field.schema.top = schemaRef.current.top;
|
||||||
|
} else {
|
||||||
|
field.schema.top = schemaRef.current;
|
||||||
|
}
|
||||||
|
|
||||||
const id = field.id || `control${tabs[group].length}`;
|
if(!_.isUndefined(field.fixedRows)) {
|
||||||
if(visible && !disabled && !firstEleID.current) {
|
canAdd = false;
|
||||||
firstEleID.current = field.id;
|
canDelete = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentControl = <MappedFormControl
|
const ctrlProps = {
|
||||||
inputRef={(ele)=>{
|
key: field.id, ...field,
|
||||||
if(firstEleRef && firstEleID.current === field.id) {
|
value: value[field.id] || [], viewHelperProps: viewHelperProps,
|
||||||
if(typeof firstEleRef == 'function') {
|
schema: field.schema, accessPath: accessPath.concat(field.id), dataDispatch: dataDispatch,
|
||||||
firstEleRef(ele);
|
containerClassName: 'FormView-controlRow',
|
||||||
} else {
|
canAdd: canAdd, canReorder: canReorder,
|
||||||
firstEleRef.current = ele;
|
canEdit: canEdit, canDelete: canDelete,
|
||||||
}
|
visible: visible, canAddRow: canAddRow, onDelete: field.onDelete, canSearch: field.canSearch,
|
||||||
|
expandEditOnAdd: field.expandEditOnAdd,
|
||||||
|
fixedRows: (viewHelperProps.mode == 'create' ? field.fixedRows : undefined),
|
||||||
|
addOnTop: Boolean(field.addOnTop)
|
||||||
|
};
|
||||||
|
|
||||||
|
if(CustomControl) {
|
||||||
|
tabs[group].push(<CustomControl {...ctrlProps}/>);
|
||||||
|
} else {
|
||||||
|
tabs[group].push(<DataGridView {...ctrlProps} />);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* Its a form control */
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
if(field.isFullTab) {
|
||||||
|
tabsClassname[group] ='FormView-fullSpace';
|
||||||
|
fullTabs.push(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = field.id || `control${tabs[group].length}`;
|
||||||
|
if(visible && !disabled && !firstEleID.current) {
|
||||||
|
firstEleID.current = field.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentControl = <MappedFormControl
|
||||||
|
inputRef={(ele)=>{
|
||||||
|
if(firstEleRef && firstEleID.current === field.id) {
|
||||||
|
if(typeof firstEleRef == 'function') {
|
||||||
|
firstEleRef(ele);
|
||||||
|
} else {
|
||||||
|
firstEleRef.current = ele;
|
||||||
}
|
}
|
||||||
}}
|
}
|
||||||
state={value}
|
}}
|
||||||
key={id}
|
state={value}
|
||||||
viewHelperProps={viewHelperProps}
|
key={id}
|
||||||
name={id}
|
viewHelperProps={viewHelperProps}
|
||||||
value={value[id]}
|
name={id}
|
||||||
{...field}
|
value={value[id]}
|
||||||
id={id}
|
{...field}
|
||||||
readonly={readonly}
|
id={id}
|
||||||
disabled={disabled}
|
readonly={readonly}
|
||||||
visible={visible}
|
disabled={disabled}
|
||||||
onChange={(changeValue)=>{
|
visible={visible}
|
||||||
/* Get the changes on dependent fields as well */
|
onChange={(changeValue)=>{
|
||||||
dataDispatch({
|
/* Get the changes on dependent fields as well */
|
||||||
type: SCHEMA_STATE_ACTIONS.SET_VALUE,
|
dataDispatch({
|
||||||
path: accessPath.concat(id),
|
type: SCHEMA_STATE_ACTIONS.SET_VALUE,
|
||||||
value: changeValue,
|
path: accessPath.concat(id),
|
||||||
});
|
value: changeValue,
|
||||||
}}
|
});
|
||||||
hasError={hasError}
|
}}
|
||||||
className='FormView-controlRow'
|
hasError={hasError}
|
||||||
noLabel={field.isFullTab}
|
className='FormView-controlRow'
|
||||||
memoDeps={[
|
noLabel={field.isFullTab}
|
||||||
value[id],
|
memoDeps={[
|
||||||
readonly,
|
value[id],
|
||||||
disabled,
|
readonly,
|
||||||
visible,
|
disabled,
|
||||||
hasError,
|
visible,
|
||||||
'FormView-controlRow',
|
hasError,
|
||||||
...(evalFunc(null, field.deps) || []).map((dep)=>value[dep]),
|
'FormView-controlRow',
|
||||||
]}
|
...(evalFunc(null, field.deps) || []).map((dep)=>value[dep]),
|
||||||
/>;
|
]}
|
||||||
|
/>;
|
||||||
|
|
||||||
if(field.isFullTab && field.helpMessage) {
|
if(field.isFullTab && field.helpMessage) {
|
||||||
currentControl = (<React.Fragment key={`coll-${field.id}`}>
|
currentControl = (<React.Fragment key={`coll-${field.id}`}>
|
||||||
<FormNote key={`note-${field.id}`} text={field.helpMessage}/>
|
<FormNote key={`note-${field.id}`} text={field.helpMessage}/>
|
||||||
{currentControl}
|
{currentControl}
|
||||||
</React.Fragment>);
|
</React.Fragment>);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(field.inlineNext) {
|
if(field.inlineNext) {
|
||||||
inlineComponents.push(React.cloneElement(currentControl, {
|
inlineComponents.push(React.cloneElement(currentControl, {
|
||||||
withContainer: false, controlGridBasis: 3
|
withContainer: false, controlGridBasis: 3
|
||||||
}));
|
}));
|
||||||
inlineCompGroup = group;
|
inlineCompGroup = group;
|
||||||
} else if(inlineComponents?.length > 0) {
|
} else if(inlineComponents?.length > 0) {
|
||||||
inlineComponents.push(React.cloneElement(currentControl, {
|
inlineComponents.push(React.cloneElement(currentControl, {
|
||||||
withContainer: false, controlGridBasis: 3
|
withContainer: false, controlGridBasis: 3
|
||||||
}));
|
}));
|
||||||
tabs[group].push(
|
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}`}
|
||||||
{inlineComponents}
|
className='FormView-controlRow' rowGap="8px">
|
||||||
</Grid>
|
{inlineComponents}
|
||||||
);
|
</Grid>
|
||||||
inlineComponents = [];
|
);
|
||||||
inlineCompGroup = null;
|
inlineComponents = [];
|
||||||
} else {
|
inlineCompGroup = null;
|
||||||
tabs[group].push(currentControl);
|
} else {
|
||||||
}
|
tabs[group].push(currentControl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(inlineComponents?.length > 0) {
|
if(inlineComponents?.length > 0) {
|
||||||
tabs[inlineCompGroup].push(
|
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}
|
{inlineComponents}
|
||||||
</Grid>
|
</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 sqlTabActive = false;
|
||||||
let sqlTabName = gettext('SQL');
|
let sqlTabName = gettext('SQL');
|
||||||
|
|
||||||
if(hasSQLTab) {
|
if(hasSQLTab) {
|
||||||
sqlTabActive = (Object.keys(finalTabs).length === tabValue);
|
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] = [
|
finalTabs[sqlTabName] = [
|
||||||
<SQLTab key="sqltab" active={sqlTabActive} getSQLValue={getSQLValue} />,
|
<SQLTab key="sqltab" active={sqlTabActive} getSQLValue={getSQLValue} />,
|
||||||
];
|
];
|
||||||
|
@ -428,7 +335,7 @@ export default function FormView({
|
||||||
fullTabs.push(sqlTabName);
|
fullTabs.push(sqlTabName);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(() => {
|
||||||
onTabChange?.(tabValue, Object.keys(tabs)[tabValue], sqlTabActive);
|
onTabChange?.(tabValue, Object.keys(tabs)[tabValue], sqlTabActive);
|
||||||
}, [tabValue]);
|
}, [tabValue]);
|
||||||
|
|
||||||
|
@ -437,26 +344,25 @@ export default function FormView({
|
||||||
// in that case, we could force virtualization of the collection.
|
// in that case, we could force virtualization of the collection.
|
||||||
if(isTabView) return false;
|
if(isTabView) return false;
|
||||||
|
|
||||||
const visibleEle = Object.values(finalTabs)[0].filter((c)=>c.props.visible);
|
const visibleEle = Object.values(finalTabs)[0].filter(
|
||||||
return visibleEle.length == 1
|
(c) => c.props.visible
|
||||||
&& visibleEle[0]?.type == DataGridView;
|
);
|
||||||
|
return visibleEle.length == 1 && visibleEle[0]?.type == DataGridView;
|
||||||
}, [isTabView, finalTabs]);
|
}, [isTabView, finalTabs]);
|
||||||
|
|
||||||
/* check whether form is kept hidden by visible prop */
|
// Check whether form is kept hidden by visible prop.
|
||||||
if(!_.isUndefined(visible) && !visible) {
|
if(!_.isUndefined(visible) && !visible) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(isTabView) {
|
if(isTabView) {
|
||||||
return (
|
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>
|
<Box>
|
||||||
<Tabs
|
<Tabs
|
||||||
value={tabValue}
|
value={tabValue}
|
||||||
onChange={(event, selTabValue) => {
|
onChange={(event, selTabValue) => { setTabValue(selTabValue); }}
|
||||||
setTabValue(selTabValue);
|
|
||||||
}}
|
|
||||||
variant="scrollable"
|
variant="scrollable"
|
||||||
scrollButtons="auto"
|
scrollButtons="auto"
|
||||||
action={(ref)=>ref?.updateIndicator()}
|
action={(ref)=>ref?.updateIndicator()}
|
||||||
|
@ -467,34 +373,49 @@ export default function FormView({
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
{Object.keys(finalTabs).map((tabName, i)=>{
|
{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) {
|
if(fullTabs.indexOf(tabName) == -1) {
|
||||||
contentClassName.push('FormView-nestedControl');
|
contentClassName.push('FormView-nestedControl');
|
||||||
} else {
|
} else {
|
||||||
contentClassName.push('FormView-fullControl');
|
contentClassName.push('FormView-fullControl');
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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}>
|
className={contentClassName.join(' ')} data-testid={tabName}>
|
||||||
{finalTabs[tabName]}
|
{finalTabs[tabName]}
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</StyledBox>
|
</FormContentBox>
|
||||||
);
|
);
|
||||||
} else {
|
} 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 (
|
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(' ')}
|
<TabPanel value={tabValue} index={0} classNameRoot={[isSingleCollection ? 'FormView-singleCollectionPanel' : 'FormView-nonTabPanel',className].join(' ')}
|
||||||
className={contentClassName.join(' ')}>
|
className={contentClassName.join(' ')}>
|
||||||
{Object.keys(finalTabs).map((tabName)=>{
|
{Object.keys(finalTabs).map((tabName) => {
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={tabName}>{finalTabs[tabName]}</React.Fragment>
|
<React.Fragment key={tabName}>
|
||||||
|
{finalTabs[tabName]}
|
||||||
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</TabPanel>
|
</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.filterGroups = []; // If set, these groups will be filtered out
|
||||||
this.informText = null; // Inform text to show after save, this only saves it
|
this.informText = null; // Inform text to show after save, this only saves it
|
||||||
this._top = null;
|
this._top = null;
|
||||||
|
|
||||||
|
this._state = null;
|
||||||
|
this._id = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Top schema is helpful if this is used as child */
|
/* Top schema is helpful if this is used as child */
|
||||||
|
@ -42,8 +45,19 @@ export default class BaseUISchema {
|
||||||
return this._origData || {};
|
return this._origData || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The session data, can be useful but setting this will not affect UI
|
set state(state) {
|
||||||
this._sessData is set by SchemaView directly. set sessData should not be allowed anywhere */
|
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() {
|
get sessData() {
|
||||||
return this._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='') {
|
pgRespErrorNotify(error, prefixMsg='') {
|
||||||
if (error.response?.status === 410) {
|
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 {
|
} else {
|
||||||
this.error(prefixMsg + ' ' + parseApiError(error));
|
this.error(prefixMsg + ' ' + parseApiError(error));
|
||||||
}
|
}
|
||||||
|
@ -163,7 +166,7 @@ class Notifier {
|
||||||
return onJSONResult();
|
return onJSONResult();
|
||||||
}
|
}
|
||||||
this.alert(promptmsg, msg.replace(new RegExp(/\r?\n/, 'g'), '<br />'));
|
this.alert(promptmsg, msg.replace(new RegExp(/\r?\n/, 'g'), '<br />'));
|
||||||
onJSONResult('ALERT_CALLED');
|
onJSONResult?.('ALERT_CALLED');
|
||||||
}
|
}
|
||||||
|
|
||||||
alert(title, text, onOkClick, okLabel=gettext('OK')) {
|
alert(title, text, onOkClick, okLabel=gettext('OK')) {
|
||||||
|
|
|
@ -304,9 +304,9 @@ class PGUtilitiesBackupFeatureTest(BaseFeatureTest):
|
||||||
existing_path = path_input.get_property("value")
|
existing_path = path_input.get_property("value")
|
||||||
if existing_path != default_binary_path[serv]:
|
if existing_path != default_binary_path[serv]:
|
||||||
path_already_set = False
|
path_already_set = False
|
||||||
self.page.clear_edit_box(path_input)
|
self.page.fill_input(
|
||||||
path_input.click()
|
path_input, default_binary_path[serv]
|
||||||
path_input.send_keys(default_binary_path[serv])
|
)
|
||||||
else:
|
else:
|
||||||
print('Binary path Key is Incorrect')
|
print('Binary path Key is Incorrect')
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -851,23 +851,25 @@ class PgadminPage:
|
||||||
self.driver.execute_script(
|
self.driver.execute_script(
|
||||||
"arguments[0].dispatchEvent(new Event('blur'));", field)
|
"arguments[0].dispatchEvent(new Event('blur'));", field)
|
||||||
|
|
||||||
def fill_input(self, field, field_content, input_keys=False,
|
def fill_input(
|
||||||
key_after_input=Keys.ARROW_DOWN):
|
self, field, field_content, input_keys=False,
|
||||||
try:
|
key_after_input=Keys.ARROW_DOWN
|
||||||
attempt = 0
|
):
|
||||||
for attempt in range(0, 3):
|
for attempt in range(0, 3):
|
||||||
|
try:
|
||||||
field.click()
|
field.click()
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
time.sleep(.2)
|
time.sleep(.2)
|
||||||
if attempt == 2:
|
if attempt == 2:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
# Use send keys if input_keys true, else use javascript to set content
|
# Use send keys if input_keys true, else use javascript to set content
|
||||||
if input_keys:
|
if input_keys:
|
||||||
backspaces = [Keys.BACKSPACE] * len(field.get_attribute('value'))
|
# Clear the existing content first
|
||||||
field.send_keys(backspaces)
|
self.clear_edit_box(field)
|
||||||
field.send_keys(str(field_content))
|
# Send the keys one by one.
|
||||||
# self.wait_for_input_by_element(field, field_content)
|
[field.send_keys(c) for c in str(field_content)]
|
||||||
else:
|
else:
|
||||||
self.driver.execute_script("arguments[0].value = arguments[1]",
|
self.driver.execute_script("arguments[0].value = arguments[1]",
|
||||||
field, field_content)
|
field, field_content)
|
||||||
|
|
|
@ -110,8 +110,8 @@ describe('SchemaView', ()=>{
|
||||||
});
|
});
|
||||||
|
|
||||||
it('onReset after change', async ()=>{
|
it('onReset after change', async ()=>{
|
||||||
onDataChange.mockClear();
|
|
||||||
await simulateChanges();
|
await simulateChanges();
|
||||||
|
onDataChange.mockClear();
|
||||||
let confirmSpy = jest.spyOn(pgAdmin.Browser.notifier, 'confirm');
|
let confirmSpy = jest.spyOn(pgAdmin.Browser.notifier, 'confirm');
|
||||||
await user.click(ctrl.container.querySelector('[data-test="Reset"]'));
|
await user.click(ctrl.container.querySelector('[data-test="Reset"]'));
|
||||||
/* Press OK */
|
/* Press OK */
|
||||||
|
|
Loading…
Reference in New Issue