Implemented utilities in React to make porting easier for pgAdmin tools.

This commit is contained in:
Aditya Toshniwal
2022-02-11 10:36:24 +05:30
committed by Akshay Joshi
parent 76a4dee451
commit bc4e8a3c82
46 changed files with 3100 additions and 281 deletions

View File

@@ -11,6 +11,7 @@
"@babel/eslint-parser": "^7.12.13",
"@babel/eslint-plugin": "^7.12.13",
"@babel/plugin-proposal-object-rest-spread": "^7.10.1",
"@babel/plugin-syntax-jsx": "^7.16.0",
"@babel/preset-env": "^7.10.2",
"@babel/preset-typescript": "^7.8.3",
"@emotion/core": "^10.0.14",
@@ -18,6 +19,7 @@
"@emotion/react": "^11.1.5",
"@emotion/styled": "^10.0.14",
"@emotion/utils": "^1.0.0",
"@svgr/webpack": "^5.5.0",
"@wojtekmaj/enzyme-adapter-react-17": "^0.4.1",
"autoprefixer": "^10.2.4",
"axios-mock-adapter": "^1.17.0",
@@ -42,7 +44,7 @@
"is-docker": "^2.1.1",
"jasmine-core": "^3.6.0",
"jasmine-enzyme": "^7.1.2",
"karma": "^6.3.2",
"karma": "^6.3.15",
"karma-babel-preprocessor": "^8.0.0",
"karma-browserify": "^8.0.0",
"karma-chrome-launcher": "^3.1.0",
@@ -63,7 +65,7 @@
"sass-resources-loader": "^2.2.1",
"style-loader": "^2.0.0",
"stylis": "^4.0.7",
"svgo": "^1.1.1",
"svgo": "^2.7.0",
"svgo-loader": "^2.2.0",
"terser-webpack-plugin": "^5.1.1",
"typescript": "^3.2.2",
@@ -86,11 +88,13 @@
"@material-ui/pickers": "^3.2.10",
"@projectstorm/react-diagrams": "^6.6.1",
"@simonwep/pickr": "^1.5.1",
"@szhsin/react-menu": "^2.2.0",
"@tippyjs/react": "^4.2.0",
"@types/classnames": "^2.2.6",
"@types/react": "^16.7.18",
"@types/react-dom": "^16.0.11",
"acitree": "git+https://github.com/imsurinder90/jquery-aciTree.git#rc.7",
"ajv": "^8.8.2",
"alertifyjs": "git+https://github.com/EnterpriseDB/AlertifyJS/#72c1d794f5b6d4ec13a68d123c08f19021afe263",
"aspen-decorations": "^1.0.2",
"axios": "^0.21.1",
@@ -103,12 +107,14 @@
"bootstrap": "^4.3.1",
"bootstrap-datepicker": "^1.8.0",
"bootstrap4-toggle": "^3.6.1",
"brace": "^0.11.1",
"browserfs": "^1.4.3",
"chart.js": "^2.9.3",
"classnames": "^2.2.6",
"closest": "^0.0.1",
"codemirror": "^5.59.2",
"context-menu": "^2.0.0",
"copy-to-clipboard": "^3.3.1",
"css-loader": "^5.0.1",
"cssnano": "^5.0.2",
"dagre": "^0.8.4",
@@ -126,6 +132,7 @@
"jquery-ui": "^1.13.0",
"json-bignumber": "^1.0.1",
"jsoneditor": "^9.5.4",
"jsoneditor-react": "^3.1.1",
"karma-coverage": "^2.0.3",
"leaflet": "^1.5.1",
"lodash": "4.*",
@@ -141,6 +148,7 @@
"pgadmin4-tree": "git+https://github.com/EnterpriseDB/pgadmin4-treeview/#bf7ac7be65898883e3e05c9733426152a1da6422",
"postcss": "^8.2.15",
"raf": "^3.4.1",
"rc-dock": "^3.2.9",
"react": "^17.0.1",
"react-aspen": "^1.1.0",
"react-checkbox-tree": "^1.7.2",
@@ -148,6 +156,7 @@
"react-draggable": "^4.4.4",
"react-select": "^4.2.1",
"react-table": "^7.6.3",
"react-timer-hook": "^3.0.5",
"react-virtualized-auto-sizer": "^1.0.6",
"react-window": "^1.8.5",
"select2": "^4.0.13",

View File

@@ -47,14 +47,24 @@ export function getNodeView(nodeType, treeNodeInfo, actionType, itemNodeData, fo
/* Called when dialog is opened in edit mode, promise required */
let initData = ()=>new Promise((resolve, reject)=>{
api.get(url(false))
.then((res)=>{
resolve(res.data);
})
.catch((err)=>{
onError(err);
reject(err);
});
if(actionType === 'create') {
resolve({});
} else {
api.get(url(false))
.then((res)=>{
resolve(res.data);
})
.catch((err)=>{
if(err.response){
console.error('error resp', err.response);
} else if(err.request){
console.error('error req', err.request);
} else if(err.message){
console.error('error msg', err.message);
}
reject(err);
});
}
});
/* on save button callback, promise required */

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 20 20" height="18px" viewBox="0 0 20 20" width="18px" fill="#000000"><g><rect fill="none" height="20" width="20"/></g><g><path d="M13,9h-1V5c0-1.1-0.9-2-2-2h0C8.9,3,8,3.9,8,5v4H7c-1.66,0-3,1.34-3,3v4c0,0.55,0.45,1,1,1h10c0.55,0,1-0.45,1-1v-4 C16,10.34,14.66,9,13,9z M15,16h-2v-1.5c0-0.28-0.22-0.5-0.5-0.5S12,14.22,12,14.5V16h-1.5v-1.5c0-0.28-0.22-0.5-0.5-0.5 s-0.5,0.22-0.5,0.5V16H8v-1.5C8,14.22,7.78,14,7.5,14S7,14.22,7,14.5V16H5v-4c0-1.1,0.9-2,2-2h6c1.1,0,2,0.9,2,2V16z"/></g></svg>

After

Width:  |  Height:  |  Size: 551 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 2h-4.18C14.4.84 13.3 0 12 0c-1.3 0-2.4.84-2.82 2H5c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm7 18H5V4h2v3h10V4h2v16z"/></svg>

After

Width:  |  Height:  |  Size: 344 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="18px" viewBox="0 0 24 24" width="18px" fill="#000000"><g><path d="M0,0h24 M24,24H0" fill="none"/><path d="M4.25,5.61C6.57,8.59,10,13,10,13v5c0,1.1,0.9,2,2,2h0c1.1,0,2-0.9,2-2v-5c0,0,3.43-4.41,5.75-7.39 C20.26,4.95,19.79,4,18.95,4H5.04C4.21,4,3.74,4.95,4.25,5.61z"/><path d="M0,0h24v24H0V0z" fill="none"/></g></svg>

After

Width:  |  Height:  |  Size: 396 B

View File

@@ -1 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#646464;}</style></defs><path class="cls-1" d="M5.42,15.76A21.75,21.75,0,0,0,12,18a47.55,47.55,0,0,0,9.06.81l1,0a16.63,16.63,0,0,1,14.8-3.11c1.48-.89,2.23-1.85,2.23-2.89v-3c0-1.08-.81-2.08-2.42-3a22,22,0,0,0-6.58-2.19,47.44,47.44,0,0,0-9-.81A47.55,47.55,0,0,0,12,4.54,22,22,0,0,0,5.42,6.73C3.81,7.66,3,8.66,3,9.74v3C3,13.83,3.81,14.84,5.42,15.76Z"/><path class="cls-1" d="M15.89,30.58a45.4,45.4,0,0,1-5.25-.77,20.41,20.41,0,0,1-7.64-3v4c0,1.08.81,2.08,2.42,3A22,22,0,0,0,12,36a44.81,44.81,0,0,0,4.56.62,16.69,16.69,0,0,1-.73-4.87C15.83,31.37,15.86,31,15.89,30.58Z"/><path class="cls-1" d="M3,17.8v4c0,1.08.81,2.09,2.42,3A21.75,21.75,0,0,0,12,27c1.4.27,2.86.47,4.38.61a16.74,16.74,0,0,1,2.81-5.86,50,50,0,0,1-8.55-1A20.58,20.58,0,0,1,3,17.8Z"/><path class="cls-1" d="M32.3,18.88A12.7,12.7,0,1,0,45,31.58,12.74,12.74,0,0,0,32.3,18.88Zm7.94,10-9.36,9.36a1,1,0,0,1-1.11.16L24.37,33a.76.76,0,0,1,0-1.11l1.11-1.27a.76.76,0,0,1,1.11,0l3.65,3.65L38,26.65a.78.78,0,0,1,1.11,0l1.11,1.12A.76.76,0,0,1,40.24,28.88Z"/></svg>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs></defs><path class="cls-1" d="M5.42,15.76A21.75,21.75,0,0,0,12,18a47.55,47.55,0,0,0,9.06.81l1,0a16.63,16.63,0,0,1,14.8-3.11c1.48-.89,2.23-1.85,2.23-2.89v-3c0-1.08-.81-2.08-2.42-3a22,22,0,0,0-6.58-2.19,47.44,47.44,0,0,0-9-.81A47.55,47.55,0,0,0,12,4.54,22,22,0,0,0,5.42,6.73C3.81,7.66,3,8.66,3,9.74v3C3,13.83,3.81,14.84,5.42,15.76Z"/><path class="cls-1" d="M15.89,30.58a45.4,45.4,0,0,1-5.25-.77,20.41,20.41,0,0,1-7.64-3v4c0,1.08.81,2.08,2.42,3A22,22,0,0,0,12,36a44.81,44.81,0,0,0,4.56.62,16.69,16.69,0,0,1-.73-4.87C15.83,31.37,15.86,31,15.89,30.58Z"/><path class="cls-1" d="M3,17.8v4c0,1.08.81,2.09,2.42,3A21.75,21.75,0,0,0,12,27c1.4.27,2.86.47,4.38.61a16.74,16.74,0,0,1,2.81-5.86,50,50,0,0,1-8.55-1A20.58,20.58,0,0,1,3,17.8Z"/><path class="cls-1" d="M32.3,18.88A12.7,12.7,0,1,0,45,31.58,12.74,12.74,0,0,0,32.3,18.88Zm7.94,10-9.36,9.36a1,1,0,0,1-1.11.16L24.37,33a.76.76,0,0,1,0-1.11l1.11-1.27a.76.76,0,0,1,1.11,0l3.65,3.65L38,26.65a.78.78,0,0,1,1.11,0l1.11,1.12A.76.76,0,0,1,40.24,28.88Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M20.06 18a3.99 3.99 0 0 1-.2-.89c-.67.7-1.48 1.05-2.41 1.05c-.83 0-1.52-.24-2.05-.71c-.53-.45-.8-1.06-.8-1.79c0-.88.33-1.56 1-2.05c.67-.49 1.61-.73 2.83-.73h1.4v-.64c0-.49-.15-.88-.45-1.17c-.3-.29-.75-.43-1.33-.43c-.52 0-.95.12-1.3.36c-.35.25-.52.54-.52.89h-1.46c0-.43.15-.84.45-1.24c.28-.4.71-.71 1.22-.94c.51-.21 1.06-.35 1.69-.35c.98 0 1.74.24 2.29.73s.84 1.16.86 2.02V16c0 .8.1 1.42.3 1.88V18h-1.52m-2.4-1.12c.45 0 .88-.11 1.29-.32c.4-.21.7-.49.88-.83v-1.57H18.7c-1.77 0-2.66.47-2.66 1.41c0 .43.15.73.46.96c.3.23.68.35 1.16.35m-12.2-3.17h4.07L7.5 8.29l-2.04 5.42M6.64 6h1.72l4.71 12h-1.93l-.97-2.57H4.82L3.86 18H1.93L6.64 6z" fill="currentColor"/></svg>

After

Width:  |  Height:  |  Size: 861 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M16 16.92c-.33.05-.66.08-1 .08c-.34 0-.67-.03-1-.08v-3.51l-2.5 2.48c-.5-.39-1-.89-1.39-1.39l2.48-2.5H9.08c-.05-.33-.08-.66-.08-1c0-.34.03-.67.08-1h3.51l-2.48-2.5c.19-.25.39-.5.65-.74c.24-.26.49-.46.74-.65L14 8.59V5.08c.33-.05.66-.08 1-.08c.34 0 .67.03 1 .08v3.51l2.5-2.48c.5.39 1 .89 1.39 1.39L17.41 10h3.51c.05.33.08.66.08 1c0 .34-.03.67-.08 1h-3.51l2.48 2.5c-.19.25-.39.5-.65.74c-.24.26-.49.46-.74.65L16 13.41v3.51M5 19a2 2 0 0 1 2-2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2z" fill="currentColor"/></svg>

After

Width:  |  Height:  |  Size: 711 B

View File

@@ -1 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><style>.cls-1{fill:#646464;}</style></defs><path class="cls-1" d="M5.42,15.76A21.75,21.75,0,0,0,12,18a47.55,47.55,0,0,0,9.06.81l1,0a16.63,16.63,0,0,1,14.8-3.11c1.48-.89,2.23-1.85,2.23-2.89v-3c0-1.08-.81-2.08-2.42-3a22,22,0,0,0-6.58-2.19,47.44,47.44,0,0,0-9-.81A47.55,47.55,0,0,0,12,4.54,22,22,0,0,0,5.42,6.73C3.81,7.66,3,8.66,3,9.74v3C3,13.83,3.81,14.84,5.42,15.76Z"/><path class="cls-1" d="M15.89,30.58a45.4,45.4,0,0,1-5.25-.77,20.34,20.34,0,0,1-7.64-3v4c0,1.08.81,2.08,2.42,3A22,22,0,0,0,12,36a44.81,44.81,0,0,0,4.56.62,16.69,16.69,0,0,1-.73-4.87C15.83,31.37,15.86,31,15.89,30.58Z"/><path class="cls-1" d="M3,17.8v4c0,1.08.81,2.09,2.42,3A21.75,21.75,0,0,0,12,27c1.4.27,2.86.47,4.38.61a16.74,16.74,0,0,1,2.81-5.86,50,50,0,0,1-8.55-1A20.51,20.51,0,0,1,3,17.8Z"/><path class="cls-1" d="M32.3,18.88A12.7,12.7,0,1,0,45,31.58,12.74,12.74,0,0,0,32.3,18.88Zm5.93,20.69a.63.63,0,0,1-1-.66c1.61-5.16-.77-6.52-6.28-6.6v3.12a.85.85,0,0,1-1.41.65l-6.26-5.41a.86.86,0,0,1,0-1.29L29.55,24a.85.85,0,0,1,1.41.65v2.84c5.71.07,10.24,1.21,10.24,6.62A7.25,7.25,0,0,1,38.23,39.57Z"/></svg>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs></defs><path class="cls-1" d="M5.42,15.76A21.75,21.75,0,0,0,12,18a47.55,47.55,0,0,0,9.06.81l1,0a16.63,16.63,0,0,1,14.8-3.11c1.48-.89,2.23-1.85,2.23-2.89v-3c0-1.08-.81-2.08-2.42-3a22,22,0,0,0-6.58-2.19,47.44,47.44,0,0,0-9-.81A47.55,47.55,0,0,0,12,4.54,22,22,0,0,0,5.42,6.73C3.81,7.66,3,8.66,3,9.74v3C3,13.83,3.81,14.84,5.42,15.76Z"/><path class="cls-1" d="M15.89,30.58a45.4,45.4,0,0,1-5.25-.77,20.34,20.34,0,0,1-7.64-3v4c0,1.08.81,2.08,2.42,3A22,22,0,0,0,12,36a44.81,44.81,0,0,0,4.56.62,16.69,16.69,0,0,1-.73-4.87C15.83,31.37,15.86,31,15.89,30.58Z"/><path class="cls-1" d="M3,17.8v4c0,1.08.81,2.09,2.42,3A21.75,21.75,0,0,0,12,27c1.4.27,2.86.47,4.38.61a16.74,16.74,0,0,1,2.81-5.86,50,50,0,0,1-8.55-1A20.51,20.51,0,0,1,3,17.8Z"/><path class="cls-1" d="M32.3,18.88A12.7,12.7,0,1,0,45,31.58,12.74,12.74,0,0,0,32.3,18.88Zm5.93,20.69a.63.63,0,0,1-1-.66c1.61-5.16-.77-6.52-6.28-6.6v3.12a.85.85,0,0,1-1.41.65l-6.26-5.41a.86.86,0,0,1,0-1.29L29.55,24a.85.85,0,0,1,1.41.65v2.84c5.71.07,10.24,1.21,10.24,6.62A7.25,7.25,0,0,1,38.23,39.57Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -186,6 +186,9 @@ export default function FormView({
if(field.depChange) {
depListener.addDepListener(source, accessPath.concat(field.id), field.depChange);
}
if(field.depChange || field.deferredDepChange) {
depListener.addDepListener(source, accessPath.concat(field.id), field.depChange, field.deferredDepChange);
}
});
});
return ()=>{
@@ -288,7 +291,7 @@ export default function FormView({
if(visible && !disabled && !firstEleID.current) {
firstEleID.current = field.id;
}
tabs[group].push(
useMemo(()=><MappedFormControl
inputRef={(ele)=>{

View File

@@ -11,7 +11,7 @@ import React, { useCallback } from 'react';
import _ from 'lodash';
import { FormInputText, FormInputSelect, FormInputSwitch, FormInputCheckbox, FormInputColor,
FormInputFileSelect, FormInputToggle, InputSwitch, FormInputSQL, FormNote, FormInputDateTimePicker, PlainString,
FormInputFileSelect, FormInputToggle, InputSwitch, FormInputSQL, FormNote, FormInputDateTimePicker, PlainString, InputSQL,
InputSelect, InputText, InputCheckbox, InputDateTimePicker } from '../components/FormComponents';
import Privilege from '../components/Privilege';
import { evalFunc } from 'sources/utils';
@@ -111,6 +111,10 @@ function MappedCellControlBase({cell, value, id, optionsLoaded, onCellChange, vi
onCellChange && onCellChange(val);
}, []);
const onSqlChange = useCallback((val) => {
onCellChange && onCellChange(val);
}, []);
/* Some grid cells are based on options selected in other cells.
* lets trigger a re-render for the row if optionsLoaded
*/
@@ -146,6 +150,8 @@ function MappedCellControlBase({cell, value, id, optionsLoaded, onCellChange, vi
return <Privilege name={name} value={value} onChange={onTextChange} {...props}/>;
case 'datetimepicker':
return <InputDateTimePicker name={name} value={value} onChange={onTextChange} {...props}/>;
case 'sql':
return <InputSQL name={name} value={value} onChange={onSqlChange} {...props} />;
default:
return <PlainString value={value} {...props} />;
}

View File

@@ -16,7 +16,7 @@ export default class BaseUISchema {
constructor(defaults) {
/* Pass the initial data to constructor so that
they will set to defaults */
this._defaults = defaults;
this._defaults = defaults || {};
this.keys = null; // If set, other fields except keys will be filtered
this.filterGroups = []; // If set, these groups will be filtered out

View File

@@ -487,42 +487,34 @@ function SchemaDialogView({
}, [sessData.__deferred__?.length]);
useEffect(()=>{
let unmounted = false;
/* Docker on load focusses itself, so our focus should execute later */
let focusTimeout = setTimeout(()=>{
firstEleRef.current && firstEleRef.current.focus();
}, 250);
/* Re-triggering focus on already focussed loses the focus */
if(viewHelperProps.mode === 'edit') {
setLoaderText('Loading...');
/* If its an edit mode, get the initial data using getInitData
getInitData should be a promise */
if(!getInitData) {
throw new Error('getInitData must be passed for edit');
setLoaderText('Loading...');
/* Get the initial data using getInitData */
/* If its an edit mode, getInitData should be present and a promise */
if(!getInitData && viewHelperProps.mode === 'edit') {
throw new Error('getInitData must be passed for edit');
}
let initDataPromise = (getInitData && getInitData()) || Promise.resolve({});
initDataPromise.then((data)=>{
if(unmounted) {
return;
}
getInitData && getInitData().then((data)=>{
data = data || {};
data = data || {};
if(viewHelperProps.mode === 'edit') {
/* Set the origData to incoming data, useful for comparing and reset */
schema.origData = prepareData(data || {});
schema.initialise(schema.origData);
sessDispatch({
type: SCHEMA_STATE_ACTIONS.INIT,
payload: schema.origData,
});
setFormReady(true);
setLoaderText('');
}).catch((err)=>{
setLoaderText('');
if (err.response && err.response.data && err.response.data.errormsg) {
Notify.alert(
gettext(err.response.statusText),
gettext(err.response.data.errormsg)
);
}
});
} else {
/* Use the defaults as the initital data */
schema.origData = prepareData(schema.defaults, true);
} else {
/* In create mode, merge with defaults */
schema.origData = prepareData({
...schema.defaults,
...data,
}, true);
}
schema.initialise(schema.origData);
sessDispatch({
type: SCHEMA_STATE_ACTIONS.INIT,
@@ -530,10 +522,23 @@ function SchemaDialogView({
});
setFormReady(true);
setLoaderText('');
}
}).catch((err)=>{
if(unmounted) {
return;
}
setLoaderText('');
if (err.response && err.response.data && err.response.data.errormsg) {
Notify.alert(
gettext(err.response.statusText),
gettext(err.response.data.errormsg)
);
}
});
/* Clear the focus timeout if unmounted */
return ()=>clearTimeout(focusTimeout);
return ()=>{
unmounted = true;
clearTimeout(focusTimeout);
};
}, []);
useEffect(()=>{
@@ -700,7 +705,7 @@ function SchemaDialogView({
<Loader message={loaderText}/>
<FormView value={sessData} viewHelperProps={viewHelperProps}
schema={schema} accessPath={[]} dataDispatch={sessDispatchWithListener}
hasSQLTab={props.hasSQL} getSQLValue={getSQLValue} firstEleRef={firstEleRef} isTabView={isTabView} />
hasSQLTab={props.hasSQL} getSQLValue={getSQLValue} firstEleRef={firstEleRef} isTabView={isTabView} className={props.formClassName} />
<FormFooterMessage type={MESSAGE_TYPE.ERROR} message={formErr.message}
onClose={onErrClose} />
</Box>
@@ -754,6 +759,7 @@ SchemaDialogView.propTypes = {
resetKey: PropTypes.any,
customSaveBtnName: PropTypes.string,
customSaveBtnIconType: PropTypes.string,
formClassName: CustomPropTypes.className,
};
const usePropsStyles = makeStyles((theme)=>({

View File

@@ -72,12 +72,12 @@ basicSettings = createMuiTheme(basicSettings, {
},
MuiButton: {
root: {
textTransform: 'none,',
textTransform: 'none',
padding: basicSettings.spacing(0.5, 1.5),
'&.Mui-disabled': {
opacity: 0.65,
opacity: 0.60,
},
'&.MuiButton-outlinedSizeSmall': {
'&.MuiButton-sizeSmall, &.MuiButton-outlinedSizeSmall, &.MuiButton-containedSizeSmall': {
height: '28px',
fontSize: '0.875rem',
'& .MuiSvgIcon-root': {
@@ -111,7 +111,7 @@ basicSettings = createMuiTheme(basicSettings, {
resize: 'vertical',
},
adornedEnd: {
paddingRight: basicSettings.spacing(1.5),
paddingRight: basicSettings.spacing(0.75),
}
},
MuiAccordion: {
@@ -184,6 +184,16 @@ basicSettings = createMuiTheme(basicSettings, {
root: {
fontSize: 14,
}
},
MuiSelect: {
selectMenu: {
minHeight: 'unset',
},
select:{
'&:focus':{
backgroundColor: 'unset',
}
}
}
},
transitions: {
@@ -220,7 +230,7 @@ basicSettings = createMuiTheme(basicSettings, {
},
MuiListItem: {
disableGutters: true,
}
},
},
});
@@ -321,7 +331,10 @@ function getFinalTheme(baseTheme) {
color: baseTheme.palette.text.muted,
backgroundColor: baseTheme.otherVars.inputDisabledBg,
},
}
'&:focus': {
outline: '0 !important',
}
},
},
MuiIconButton: {
root: {

View File

@@ -92,13 +92,16 @@ export default function(basicSettings) {
inputBorderColor: '#dde0e6',
inputDisabledBg: '#f3f5f9',
headerBg: '#fff',
activeBorder: '#326690',
activeColor: '#326690',
tableBg: '#fff',
activeStepBg: '#326690',
activeStepFg: '#FFFFFF',
stepBg: '#ddd',
stepFg: '#000',
toggleBtnBg: '#000'
toggleBtnBg: '#000',
editorToolbarBg: '#ebeef3',
datagridBg: '#fff',
}
});
}

View File

@@ -28,7 +28,7 @@ export function parseApiError(error) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
if(error.response.headers['content-type'] == 'application/json') {
return error.response.data.errormsg;
return `INTERNAL SERVER ERROR: ${error.response.data.errormsg}`;
} else {
return error.response.statusText;
}
@@ -37,8 +37,10 @@ export function parseApiError(error) {
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
return gettext('Connection to pgAdmin server has been lost');
} else {
} else if(error.message) {
// Something happened in setting up the request that triggered an Error
return error.message;
} else {
return error;
}
}

View File

@@ -7,20 +7,16 @@
//
//////////////////////////////////////////////////////////////
import { Button, makeStyles, Tooltip } from '@material-ui/core';
import { Button, ButtonGroup, makeStyles, Tooltip } from '@material-ui/core';
import React, { forwardRef } from 'react';
import clsx from 'clsx';
import PropTypes from 'prop-types';
import CustomPropTypes from '../custom_prop_types';
import ShortcutTitle from './ShortcutTitle';
const useStyles = makeStyles((theme)=>({
primaryButton: {
'&.MuiButton-outlinedSizeSmall': {
height: '28px',
'& .MuiSvgIcon-root': {
height: '0.8em',
}
},
border: '1px solid '+theme.palette.primary.main,
'&.Mui-disabled': {
color: theme.palette.primary.contrastText,
backgroundColor: theme.palette.primary.disabledMain,
@@ -35,12 +31,6 @@ const useStyles = makeStyles((theme)=>({
color: theme.palette.default.contrastText,
border: '1px solid '+theme.palette.default.borderColor,
whiteSpace: 'nowrap',
'&.MuiButton-outlinedSizeSmall': {
height: '28px',
'& .MuiSvgIcon-root': {
height: '0.8em',
}
},
'&.Mui-disabled': {
color: theme.palette.default.disabledContrastText,
borderColor: theme.palette.default.disabledBorderColor
@@ -53,6 +43,11 @@ const useStyles = makeStyles((theme)=>({
},
iconButton: {
padding: '3px 6px',
'&.MuiButton-sizeSmall, &.MuiButton-outlinedSizeSmall, &.MuiButton-containedSizeSmall': {
padding: '1px 4px',
},
},
iconButtonDefault: {
borderColor: theme.custom.icon.borderColor,
color: theme.custom.icon.contrastText,
backgroundColor: theme.custom.icon.main,
@@ -64,7 +59,15 @@ const useStyles = makeStyles((theme)=>({
'&:hover': {
backgroundColor: theme.custom.icon.hoverMain,
color: theme.custom.icon.hoverContrastText,
}
},
},
splitButton: {
'&.MuiButton-sizeSmall, &.MuiButton-outlinedSizeSmall, &.MuiButton-containedSizeSmall': {
width: '20px',
'& svg': {
height: '0.8em',
}
},
},
xsButton: {
padding: '2px 1px',
@@ -89,7 +92,7 @@ export const PrimaryButton = forwardRef((props, ref)=>{
}
noBorder && allClassName.push(classes.noBorder);
return (
<Button ref={ref} variant="contained" color="primary" size={size} className={clsx(allClassName)} {...otherProps}>{children}</Button>
<Button ref={ref} size={size} className={clsx(allClassName)} {...otherProps} variant="contained" color="primary" >{children}</Button>
);
});
PrimaryButton.displayName = 'PrimaryButton';
@@ -111,7 +114,7 @@ export const DefaultButton = forwardRef((props, ref)=>{
}
noBorder && allClassName.push(classes.noBorder);
return (
<Button ref={ref} variant="outlined" color="default" size={size} className={clsx(allClassName)} {...otherProps}>{children}</Button>
<Button variant="outlined" color="default" ref={ref} size={size} className={clsx(allClassName)} {...otherProps} >{children}</Button>
);
});
DefaultButton.displayName = 'DefaultButton';
@@ -122,24 +125,77 @@ DefaultButton.propTypes = {
className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
};
/* pgAdmin Icon button, takes Icon component as input */
export const PgIconButton = forwardRef(({icon, title, className, ...props}, ref)=>{
export const PgIconButton = forwardRef(({icon, title, shortcut, accessKey, className, splitButton, style, color, ...props}, ref)=>{
const classes = useStyles();
let shortcutTitle = null;
if(accessKey || shortcut) {
shortcutTitle = <ShortcutTitle title={title} accessKey={accessKey} shortcut={shortcut}/>;
}
/* Tooltip does not work for disabled items */
return (
<Tooltip title={title || ''} aria-label={title || ''}>
<span>
<DefaultButton ref={ref} style={{minWidth: 0}} className={clsx(classes.iconButton, className)} {...props}>
if(props.disabled) {
if(color == 'primary') {
return (
<PrimaryButton ref={ref} style={{minWidth: 0, ...style}}
className={clsx(classes.iconButton, (splitButton ? classes.splitButton : ''), className)} {...props}>
{icon}
</PrimaryButton>
);
} else {
return (
<DefaultButton ref={ref} style={{minWidth: 0, ...style}}
className={clsx(classes.iconButton, classes.iconButtonDefault, (splitButton ? classes.splitButton : ''), className)} {...props}>
{icon}
</DefaultButton>
</span>
</Tooltip>
);
);
}
} else {
if(color == 'primary') {
return (
<Tooltip title={shortcutTitle || title || ''} aria-label={title || ''}>
<PrimaryButton ref={ref} style={{minWidth: 0, ...style}}
className={clsx(classes.iconButton, (splitButton ? classes.splitButton : ''), className)} {...props}>
{icon}
</PrimaryButton>
</Tooltip>
);
} else {
return (
<Tooltip title={shortcutTitle || title || ''} aria-label={title || ''}>
<DefaultButton ref={ref} style={{minWidth: 0, ...style}}
className={clsx(classes.iconButton, classes.iconButtonDefault, (splitButton ? classes.splitButton : ''), className)} {...props}>
{icon}
</DefaultButton>
</Tooltip>
);
}
}
});
PgIconButton.displayName = 'PgIconButton';
PgIconButton.propTypes = {
icon: CustomPropTypes.children,
title: PropTypes.string.isRequired,
shortcut: CustomPropTypes.shortcut,
accessKey: PropTypes.string,
className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
style: PropTypes.object,
color: PropTypes.oneOf(['primary', 'default', undefined]),
disabled: PropTypes.bool,
splitButton: PropTypes.bool,
};
export const PgButtonGroup = forwardRef(({children, ...props}, ref)=>{
/* Tooltip does not work for disabled items */
return (
<ButtonGroup disableElevation innerRef={ref} {...props}>
{children}
</ButtonGroup>
);
});
PgButtonGroup.displayName = 'PgButtonGroup';
PgButtonGroup.propTypes = {
children: CustomPropTypes.children,
};

View File

@@ -7,11 +7,258 @@
//
//////////////////////////////////////////////////////////////
import React, { useEffect, useMemo, useRef } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import {default as OrigCodeMirror} from 'bundled_codemirror';
import {useOnScreen} from 'sources/custom_hooks';
import PropTypes from 'prop-types';
import CustomPropTypes from '../custom_prop_types';
import pgAdmin from 'sources/pgadmin';
import gettext from 'sources/gettext';
import { Box, InputAdornment, makeStyles } from '@material-ui/core';
import clsx from 'clsx';
import { InputText } from './FormComponents';
import { PgIconButton } from './Buttons';
import CloseIcon from '@material-ui/icons/CloseRounded';
import ArrowDownwardRoundedIcon from '@material-ui/icons/ArrowDownwardRounded';
import ArrowUpwardRoundedIcon from '@material-ui/icons/ArrowUpwardRounded';
import SwapHorizRoundedIcon from '@material-ui/icons/SwapHorizRounded';
import SwapCallsRoundedIcon from '@material-ui/icons/SwapCallsRounded';
import _ from 'lodash';
import { RegexIcon, FormatCaseIcon } from './ExternalIcon';
import { isMac } from '../keyboard_shortcuts';
const useStyles = makeStyles((theme)=>({
root: {
position: 'relative',
},
findDialog: {
position: 'absolute',
zIndex: 99,
right: '4px',
...theme.mixins.panelBorder.all,
borderTop: 'none',
padding: '2px 4px',
width: '250px',
backgroundColor: theme.palette.background.default,
},
marginTop: {
marginTop: '0.25rem',
}
}));
function parseString(string) {
return string.replace(/\\([nrt\\])/g, function(match, ch) {
if (ch == 'n') return '\n';
if (ch == 'r') return '\r';
if (ch == 't') return '\t';
if (ch == '\\') return '\\';
return match;
});
}
function parseQuery(query, useRegex=false, matchCase=false) {
if (useRegex) {
query = new RegExp(query, matchCase ? 'g': 'gi');
} else {
query = parseString(query);
if(!matchCase) {
query = query.toLowerCase();
}
}
if (typeof query == 'string' ? query == '' : query.test(''))
query = /x^/;
return query;
}
function searchOverlay(query, matchCase) {
return {
token: typeof query == 'string' ?
(stream) =>{
var matchIndex = (matchCase ? stream.string : stream.string.toLowerCase()).indexOf(query, stream.pos);
if(matchIndex == -1) {
stream.skipToEnd();
} else if(matchIndex == stream.pos) {
stream.pos += query.length;
return 'searching';
} else {
stream.pos = matchIndex;
}
} : (stream) => {
query.lastIndex = stream.pos;
var match = query.exec(stream.string);
if (match && match.index == stream.pos) {
stream.pos += match[0].length || 1;
return 'searching';
} else if (match) {
stream.pos = match.index;
} else {
stream.skipToEnd();
}
}
};
}
export const CodeMirrorInstancType = PropTypes.shape({
getCursor: PropTypes.func,
getSearchCursor: PropTypes.func,
removeOverlay: PropTypes.func,
addOverlay: PropTypes.func,
setSelection: PropTypes.func,
scrollIntoView: PropTypes.func,
});
export function FindDialog({editor, show, replace, onClose}) {
const [findVal, setFindVal] = useState('');
const [replaceVal, setReplaceVal] = useState('');
const [useRegex, setUseRegex] = useState(false);
const [matchCase, setMatchCase] = useState(false);
const findInputRef = useRef();
const highlightsearch = useRef();
const searchCursor = useRef();
const classes = useStyles();
const search = ()=>{
if(editor) {
let query = parseQuery(findVal, useRegex, matchCase);
searchCursor.current = editor.getSearchCursor(query, editor.getCursor(true), !matchCase);
if(findVal != '') {
editor.removeOverlay(highlightsearch.current);
highlightsearch.current = searchOverlay(query, matchCase);
editor.addOverlay(highlightsearch.current);
onFindNext();
} else {
editor.removeOverlay(highlightsearch.current);
}
}
};
useEffect(()=>{
if(show) {
findInputRef.current && findInputRef.current.select();
search();
}
}, [show]);
useEffect(()=>{
search();
}, [findVal, useRegex, matchCase]);
const clearAndClose = ()=>{
editor.removeOverlay(highlightsearch.current);
onClose();
};
const toggle = (name)=>{
if(name == 'regex') {
setUseRegex((prev)=>!prev);
} else if(name == 'case') {
setMatchCase((prev)=>!prev);
}
};
const onFindEnter = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
if(e.shiftKey) {
onFindPrev();
} else {
onFindNext();
}
}
};
const onReplaceEnter = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
onReplace();
}
};
const onEscape = (e)=>{
if (e.key === 'Escape') {
e.preventDefault();
clearAndClose();
}
};
const onFindNext = ()=>{
if(searchCursor.current && searchCursor.current.find()) {
editor.setSelection(searchCursor.current.from(), searchCursor.current.to());
editor.scrollIntoView({
from: searchCursor.current.from(),
to: searchCursor.current.to()
}, 20);
}
};
const onFindPrev = ()=>{
if(searchCursor.current && searchCursor.current.find(true)) {
editor.setSelection(searchCursor.current.from(), searchCursor.current.to());
editor.scrollIntoView({
from: searchCursor.current.from(),
to: searchCursor.current.to()
}, 20);
}
};
const onReplace = ()=>{
searchCursor.current.replace(replaceVal);
onFindNext();
};
const onReplaceAll = ()=>{
while(searchCursor.current.from()) {
onReplace();
}
};
if(!editor) {
return <></>;
}
return (
<Box className={classes.findDialog} visibility={show ? 'visible' : 'hidden'} tabIndex="0" onKeyDown={onEscape}>
<InputText value={findVal}
inputRef={(ele)=>{findInputRef.current = ele;}}
onChange={(value)=>setFindVal(value)}
onKeyPress={onFindEnter}
endAdornment={
<InputAdornment position="end">
<PgIconButton data-test="case" title="Match case" icon={<FormatCaseIcon />} size="xs" noBorder
onClick={()=>toggle('case')} color={matchCase ? 'primary' : 'default'} style={{marginRight: '2px'}}/>
<PgIconButton data-test="regex" title="Use regex" icon={<RegexIcon />} size="xs" noBorder
onClick={()=>toggle('regex')} color={useRegex ? 'primary' : 'default'}/>
</InputAdornment>
}
/>
{replace &&
<InputText value={replaceVal}
className={classes.marginTop}
onChange={(value)=>setReplaceVal(value)}
onKeyPress={onReplaceEnter}
/>}
<Box display="flex" className={classes.marginTop}>
<PgIconButton title={gettext('Previous')} icon={<ArrowUpwardRoundedIcon />} size="xs" noBorder onClick={onFindPrev} />
<PgIconButton title={gettext('Next')} icon={<ArrowDownwardRoundedIcon />} size="xs" noBorder onClick={onFindNext}/>
{replace && <>
<PgIconButton title={gettext('Replace')} icon={<SwapHorizRoundedIcon style={{height: 'unset'}}/>} size="xs" noBorder onClick={onReplace} />
<PgIconButton title={gettext('Replace All')} icon={<SwapCallsRoundedIcon />} size="xs" noBorder onClick={onReplaceAll}/>
</>}
<Box marginLeft="auto">
<PgIconButton title={gettext('Close')} icon={<CloseIcon />} size="xs" noBorder onClick={clearAndClose}/>
</Box>
</Box>
</Box>
);
}
FindDialog.propTypes = {
editor: CodeMirrorInstancType,
show: PropTypes.bool,
replace: PropTypes.bool,
onClose: PropTypes.func,
};
/* React wrapper for CodeMirror */
export default function CodeMirror({currEditor, name, value, options, events, readonly, disabled, className}) {
@@ -19,13 +266,34 @@ export default function CodeMirror({currEditor, name, value, options, events, re
const editor = useRef();
const cmWrapper = useRef();
const isVisibleTrack = useRef();
const classes = useStyles();
const [[showFind, isReplace], setShowFind] = useState([false, false]);
const defaultOptions = {
tabindex: '0',
lineNumbers: true,
styleSelectedText: true,
mode: 'text/x-pgsql',
foldOptions: {
widget: '\u2026',
},
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
extraKeys: pgAdmin.Browser.editor_shortcut_keys,
dragDrop: false,
screenReaderLabel: gettext('SQL editor'),
};
useEffect(()=>{
const finalOptions = {...defaultOptions, ...options};
/* Create the object only once on mount */
editor.current = new OrigCodeMirror.fromTextArea(
taRef.current, options);
taRef.current, finalOptions);
editor.current.setValue(value);
if(!_.isEmpty(value)) {
editor.current.setValue(value);
} else {
editor.current.setValue('');
}
currEditor && currEditor(editor.current);
if(editor.current) {
try {
@@ -33,11 +301,33 @@ export default function CodeMirror({currEditor, name, value, options, events, re
} catch(e) {
cmWrapper.current = null;
}
let findKey = 'Ctrl-F', replaceKey = 'Shift-Ctrl-F';
if(isMac()) {
findKey = 'Cmd-F';
replaceKey = 'Cmd-Alt-F';
}
editor.current.addKeyMap({
[findKey]: ()=>{
setShowFind([false, false]);
setShowFind([true, false]);
},
[replaceKey]: ()=>{
if(!finalOptions.readOnly) {
setShowFind([false, false]);
setShowFind([true, true]);
}
}
});
}
Object.keys(events||{}).forEach((eventName)=>{
editor.current.on(eventName, events[eventName]);
});
return ()=>{
editor.current?.toTextArea();
};
}, []);
useEffect(()=>{
@@ -62,7 +352,11 @@ export default function CodeMirror({currEditor, name, value, options, events, re
useMemo(() => {
if(editor.current) {
if(value != editor.current.getValue()) {
editor.current.setValue(value);
if(!_.isEmpty(value)) {
editor.current.setValue(value);
} else {
editor.current.setValue('');
}
}
}
}, [value]);
@@ -75,8 +369,16 @@ export default function CodeMirror({currEditor, name, value, options, events, re
isVisibleTrack.current = false;
}
const closeFind = ()=>{
setShowFind([false, false]);
editor.current?.focus();
};
return (
<div className={className}><textarea ref={taRef} name={name} /></div>
<div className={clsx(className, classes.root)}>
<FindDialog editor={editor.current} show={showFind} replace={isReplace} onClose={closeFind}/>
<textarea ref={taRef} name={name} />
</div>
);
}

View File

@@ -0,0 +1,34 @@
import React from 'react';
import QueryToolSvg from '../../img/fonticon/query_tool.svg?svgr';
import SaveDataSvg from '../../img/fonticon/save_data_changes.svg?svgr';
import PasteSvg from '../../img/content_paste.svg?svgr';
import FilterSvg from '../../img/filter_alt_black.svg?svgr';
import ClearSvg from '../../img/cleaning_services_black.svg?svgr';
import CommitSvg from '../../img/fonticon/commit.svg?svgr';
import RollbackSvg from '../../img/fonticon/rollback.svg?svgr';
import ConnectedSvg from '../../img/fonticon/connected.svg?svgr';
import DisconnectedSvg from '../../img/fonticon/disconnected.svg?svgr';
import RegexSvg from '../../img/fonticon/regex.svg?svgr';
import FormatCaseSvg from '../../img/fonticon/format_case.svg?svgr';
import PropTypes from 'prop-types';
export default function ExternalIcon({Icon, ...props}) {
return <Icon className='MuiSvgIcon-root' {...props} />;
}
ExternalIcon.propTypes = {
Icon: PropTypes.elementType.isRequired,
};
export const QueryToolIcon = ()=><ExternalIcon Icon={QueryToolSvg} style={{height: '0.7em'}} />;
export const SaveDataIcon = ()=><ExternalIcon Icon={SaveDataSvg} style={{height: '0.7em'}} />;
export const PasteIcon = ()=><ExternalIcon Icon={PasteSvg} />;
export const FilterIcon = ()=><ExternalIcon Icon={FilterSvg} />;
export const CommitIcon = ()=><ExternalIcon Icon={CommitSvg} />;
export const RollbackIcon = ()=><ExternalIcon Icon={RollbackSvg} />;
export const ClearIcon = ()=><ExternalIcon Icon={ClearSvg} />;
export const ConnectedIcon = ()=><ExternalIcon Icon={ConnectedSvg} style={{height: '0.7em'}} />;
export const DisonnectedIcon = ()=><ExternalIcon Icon={DisconnectedSvg} style={{height: '0.7em'}} />;
export const RegexIcon = ()=><ExternalIcon Icon={RegexSvg} />;
export const FormatCaseIcon = ()=><ExternalIcon Icon={FormatCaseSvg} />;

View File

@@ -11,7 +11,7 @@
import React, { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import { Box, FormControl, OutlinedInput, FormHelperText,
Grid, IconButton, FormControlLabel, Switch, Checkbox, useTheme, InputLabel, Paper } from '@material-ui/core';
Grid, IconButton, FormControlLabel, Switch, Checkbox, useTheme, InputLabel, Paper, Select as MuiSelect } from '@material-ui/core';
import { ToggleButton, ToggleButtonGroup } from '@material-ui/lab';
import ErrorRoundedIcon from '@material-ui/icons/ErrorOutlineRounded';
import InfoRoundedIcon from '@material-ui/icons/InfoRounded';
@@ -148,7 +148,7 @@ FormInput.propTypes = {
testcid: PropTypes.any,
};
export function InputSQL({value, options, onChange, className, ...props}) {
export function InputSQL({value, onChange, className, controlProps, ...props}) {
const classes = useStyles();
const editor = useRef();
@@ -156,17 +156,13 @@ export function InputSQL({value, options, onChange, className, ...props}) {
<CodeMirror
currEditor={(obj)=>editor.current=obj}
value={value||''}
options={{
lineNumbers: true,
mode: 'text/x-pgsql',
...options,
}}
className={clsx(classes.sql, className)}
events={{
change: (cm)=>{
onChange && onChange(cm.getValue());
},
}}
{...controlProps}
{...props}
/>
);
@@ -177,15 +173,16 @@ InputSQL.propTypes = {
onChange: PropTypes.func,
readonly: PropTypes.bool,
className: CustomPropTypes.className,
controlProps: PropTypes.object,
};
export function FormInputSQL({hasError, required, label, className, helpMessage, testcid, value, controlProps, noLabel, ...props}) {
export function FormInputSQL({hasError, required, label, className, helpMessage, testcid, value, noLabel, ...props}) {
if(noLabel) {
return <InputSQL value={value} options={controlProps} {...props}/>;
return <InputSQL value={value} {...props}/>;
} else {
return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid} >
<InputSQL value={value} options={controlProps} {...props}/>
<InputSQL value={value} {...props}/>
</FormInput>
);
}
@@ -198,7 +195,6 @@ FormInputSQL.propTypes = {
helpMessage: PropTypes.string,
testcid: PropTypes.string,
value: PropTypes.string,
controlProps: PropTypes.object,
noLabel: PropTypes.bool,
change: PropTypes.func,
};
@@ -739,6 +735,17 @@ function getRealValue(options, value, creatable, formatter) {
}
return realValue;
}
export function InputSelectNonSearch({options, ...props}) {
return <MuiSelect native {...props} variant="outlined">
{(options||[]).map((o)=><option key={o.value} value={o.value}>{o.label}</option>)}
</MuiSelect>;
}
InputSelectNonSearch.propTypes = {
options: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.shape,
value: PropTypes.any,
})),
};
export const InputSelect = forwardRef(({
cid, onChange, options, readonly=false, value, controlProps={}, optionsLoaded, optionsReloadBasis, disabled, ...props}, ref) => {

View File

@@ -0,0 +1,60 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useEffect, useMemo, useRef } from 'react';
import {default as OrigJsonEditor} from 'jsoneditor.min';
import PropTypes from 'prop-types';
import CustomPropTypes from '../custom_prop_types';
/* React wrapper for JsonEditor */
export default function JsonEditor({currEditor, value, options, className}) {
const eleRef = useRef();
const editor = useRef();
const defaultOptions = {
modes: ['code', 'form', 'tree','preview'],
};
useEffect(()=>{
/* Create the object only once on mount */
editor.current = new OrigJsonEditor(eleRef.current, {
...defaultOptions,
...options,
onChange: ()=>{
let currVal = editor.current.getText();
if(currVal == '') {
currVal = null;
}
options.onChange(currVal);
}
});
editor.current.setText(value);
currEditor && currEditor(editor.current);
editor.current.focus();
return ()=>editor.current?.destroy();
}, []);
useMemo(() => {
if(editor.current) {
if(value != editor.current.getText()) {
editor.current.setText(value ?? '');
}
}
}, [value]);
return (
<div ref={eleRef} className={className}></div>
);
}
JsonEditor.propTypes = {
currEditor: PropTypes.func,
value: PropTypes.string,
options: PropTypes.object,
className: CustomPropTypes.className,
};

View File

@@ -40,13 +40,13 @@ const useStyles = makeStyles((theme)=>({
}
}));
export default function Loader({message}) {
export default function Loader({message, style}) {
const classes = useStyles();
if(!message) {
return <></>;
}
return (
<Box className={classes.root}>
<Box className={classes.root} style={style}>
<Box className={classes.loaderRoot}>
<CircularProgress className={classes.loader} />
<Typography className={classes.message}>{message}</Typography>
@@ -57,4 +57,5 @@ export default function Loader({message}) {
Loader.propTypes = {
message: PropTypes.string,
style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
};

View File

@@ -0,0 +1,83 @@
import { makeStyles } from '@material-ui/styles';
import React from 'react';
import CheckIcon from '@material-ui/icons/Check';
import PropTypes from 'prop-types';
import {
MenuItem,
ControlledMenu,
applyStatics,
} from '@szhsin/react-menu';
export {MenuDivider as PgMenuDivider} from '@szhsin/react-menu';
import { shortcutToString } from './ShortcutTitle';
import clsx from 'clsx';
import CustomPropTypes from '../custom_prop_types';
const useStyles = makeStyles((theme)=>({
menu: {
'& .szh-menu': {
padding: '4px 0px',
zIndex: 1000,
},
'& .szh-menu__divider': {
margin: 0,
}
},
menuItem: {
display: 'flex',
padding: '4px 8px',
'&.szh-menu__item--active, &.szh-menu__item--hover': {
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
}
},
hideCheck: {
visibility: 'hidden',
},
shortcut: {
marginLeft: 'auto',
fontSize: '0.8em',
paddingLeft: '12px',
}
}));
export function PgMenu({open, className, ...props}) {
const classes = useStyles();
return (
<ControlledMenu
state={open ? 'open' : 'closed'}
{...props}
className={clsx(classes.menu, className)}
/>
);
}
PgMenu.propTypes = {
open: PropTypes.bool,
className: CustomPropTypes.className,
};
export const PgMenuItem = applyStatics(MenuItem)(({hasCheck=false, checked=false, accesskey, shortcut, children, ...props})=>{
const classes = useStyles();
let onClick = props.onClick;
if(hasCheck) {
onClick = (e)=>{
e.keepOpen = true;
props.onClick(e);
};
}
return <MenuItem {...props} onClick={onClick} className={classes.menuItem}>
{hasCheck && <CheckIcon style={checked ? {} : {visibility: 'hidden'}}/>}
{children}
{(shortcut || accesskey) && <div className={classes.shortcut}>({shortcutToString(shortcut, accesskey)})</div>}
</MenuItem>;
});
PgMenuItem.propTypes = {
hasCheck: PropTypes.bool,
checked: PropTypes.bool,
accesskey: PropTypes.string,
shortcut: CustomPropTypes.shortcut,
children: CustomPropTypes.children,
onClick: PropTypes.func,
};

View File

@@ -0,0 +1,63 @@
import React from 'react';
import { makeStyles } from '@material-ui/styles';
import PropTypes from 'prop-types';
import { isMac } from '../keyboard_shortcuts';
import _ from 'lodash';
import CustomPropTypes from '../custom_prop_types';
const useStyles = makeStyles((theme)=>({
shortcut: {
justifyContent: 'center',
marginTop: '0.125rem',
display: 'flex',
},
key: {
padding: '0 0.25rem',
border: `1px solid ${theme.otherVars.borderColor}`,
marginRight: '0.125rem',
borderRadius: theme.shape.borderRadius,
},
}));
export function shortcutToString(shortcut, accesskey=null, asArray=false) {
let keys = [];
if(accesskey) {
keys.push('Accesskey');
keys.push(_.capitalize(accesskey?.toUpperCase()));
} else if(shortcut) {
shortcut.alt && keys.push((isMac() ? 'Option' : 'Alt'));
if(isMac() && shortcut.ctrl_is_meta) {
shortcut.control && keys.push('Cmd');
} else {
shortcut.control && keys.push('Ctrl');
}
shortcut.shift && keys.push('Shift');
keys.push(_.capitalize(shortcut.key.char));
} else {
return '';
}
return asArray ? keys : keys.join(' + ');
}
/* The tooltip content to show shortcut details */
export default function ShortcutTitle({title, shortcut, accessKey}) {
const classes = useStyles();
let keys = shortcutToString(shortcut, accessKey, true);
return (
<>
<div>{title}</div>
<div className={classes.shortcut}>
{keys.map((key, i)=>{
return <div key={i} className={classes.key}>{key}</div>;
})}
</div>
</>
);
}
ShortcutTitle.propTypes = {
title: PropTypes.string,
shortcut: CustomPropTypes.shortcut,
accessKey: PropTypes.string,
};

View File

@@ -13,12 +13,9 @@ import clsx from 'clsx';
import PropTypes from 'prop-types';
import CustomPropTypes from '../custom_prop_types';
const useStyles = makeStyles((theme)=>({
export const tabPanelStyles = makeStyles((theme)=>({
root: {
height: '100%',
padding: theme.spacing(1),
overflow: 'auto',
backgroundColor: theme.palette.grey[400]
...theme.mixins.tabPanel,
},
content: {
height: '100%',
@@ -27,7 +24,7 @@ const useStyles = makeStyles((theme)=>({
/* Material UI does not have any tabpanel component, we create one for us */
export default function TabPanel({children, classNameRoot, className, value, index}) {
const classes = useStyles();
const classes = tabPanelStyles();
const active = value === index;
return (
<Box className={clsx(classes.root, classNameRoot)} component="div" hidden={!active}>

View File

@@ -1,12 +1,10 @@
import {useRef, useEffect, useState, useCallback} from 'react';
export { useStopwatch } from 'react-timer-hook';
/* React hook for setInterval */
export function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
});
savedCallback.current = callback;
useEffect(() => {
function tick() {
@@ -20,6 +18,19 @@ export function useInterval(callback, delay) {
}, [delay]);
}
export function useDelayedCaller(callback) {
let timer;
useEffect(() => {
return () => clearTimeout(timer);
}, []);
return (delay)=>{
timer = setTimeout(() => {
callback();
}, delay);
};
}
export function usePrevious(value) {
const ref = useRef();
useEffect(() => {
@@ -66,3 +77,59 @@ export function useIsMounted() {
}, []);
return useCallback(() => ref.current, []);
}
/*
shortcuts = [
{
// From the preferences
shortcut: {
'control': true,
'shift': false,
'alt': true,
'key': {
'key_code': 73,
'char': 'I',
},
},
options: {
callback: ()=>{}
enabled?: boolean optional
}
}
]
*/
export function useKeyboardShortcuts(shortcuts, eleRef) {
const shortcutsRef = useRef(shortcuts);
const matchFound = (shortcut, e)=>{
if(!shortcut) return false;
let keyCode = e.which || e.keyCode;
return shortcut.alt == e.altKey &&
shortcut.shift == e.shiftKey &&
shortcut.control == e.ctrlKey &&
shortcut.key.key_code == keyCode;
};
useEffect(()=>{
let ele = eleRef.current ?? document;
const keyupCallback = (e)=>{
for(let i=0; i<(shortcutsRef.current??[]).length; i++){
let {shortcut, options} = shortcutsRef.current[i];
if(matchFound(shortcut, e)) {
if(options.callback && (options.enabled ?? true)) {
options.callback(e);
}
break;
}
}
};
ele.addEventListener('keyup', keyupCallback);
return ()=>{
ele.removeEventListener('keyup', keyupCallback);
};
}, [eleRef.current]);
useEffect(()=>{
shortcutsRef.current = shortcuts;
}, [shortcuts]);
}

View File

@@ -21,13 +21,23 @@ const CustomPropTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
PropTypes.node,
]),
className: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
])
PropTypes.array,
]),
shortcut: PropTypes.shape({
alt: PropTypes.bool,
control: PropTypes.bool,
shift: PropTypes.bool,
key: PropTypes.shape({
char: PropTypes.string,
}),
}),
};
export default CustomPropTypes;

View File

@@ -0,0 +1,44 @@
import React from 'react';
import _ from 'lodash';
export default class EventBus {
constructor() {
this._eventListeners = [];
}
registerListener(event, callback) {
this._eventListeners = this._eventListeners || [];
this._eventListeners.push({
event: event,
callback: callback,
});
}
deregisterListener(event, callback) {
if(callback) {
this._eventListeners = this._eventListeners.filter((e)=>{
if(e.event === event) {
return e.callback.toString()!=callback.toString();
}
return e.event!=event && e.callback.toString()!=callback.toString();
});
} else {
this._eventListeners = this._eventListeners.filter((e)=>e.event!=event);
}
}
fireEvent(event, ...args) {
Promise.resolve(0).then(()=>{
let allListeners = _.filter(this._eventListeners, (e)=>e.event==event);
if(allListeners) {
for(const listener of allListeners) {
Promise.resolve(0).then(()=>{
listener.callback(...args);
});
}
}
});
}
}
export const EventBusContext = React.createContext(new EventBus());

View File

@@ -0,0 +1,179 @@
import React from 'react';
import DockLayout from 'rc-dock';
import { makeStyles } from '@material-ui/styles';
import PropTypes from 'prop-types';
import CustomPropTypes from '../custom_prop_types';
const useStyles = makeStyles((theme)=>({
docklayout: {
height: '100%',
'& .dock-tab-active': {
color: theme.otherVars.activeColor,
'&::hover': {
color: theme.otherVars.activeColor,
}
},
'& .dock-ink-bar': {
height: '3px',
backgroundColor: theme.otherVars.activeBorder,
color: theme.otherVars.activeColor,
'&.dock-ink-bar-animated': {
transition: 'none !important',
}
},
'& .dock-bar': {
paddingLeft: 0,
backgroundColor: theme.palette.background.default,
...theme.mixins.panelBorder.bottom,
},
'& .dock-panel': {
border: 'none',
'&.dock-style-dialogs': {
'&.dock-panel.dragging': {
opacity: 1,
},
'& .dock-ink-bar': {
height: '0px',
},
'& .dock-panel-drag-size-b-r': {
zIndex: 1020,
},
'& .dock-tab-active': {
color: theme.palette.text.primary,
fontWeight: 'bold',
'&::hover': {
color: theme.palette.text.primary,
}
},
}
},
'& .dock-tab': {
minWidth: 'unset',
borderBottom: 'none',
marginRight: 0,
background: 'unset',
fontWeight: 'unset',
'&::hover': {
color: 'unset',
}
},
'& .dock-vbox, & .dock-hbox .dock-vbox': {
'& .dock-divider': {
flexBasis: '1px',
transform: 'scaleY(8)',
'&::before': {
backgroundColor: theme.otherVars.borderColor,
display: 'block',
content: '""',
width: '100%',
transform: 'scaleY(0.125)',
height: '1px',
}
}
},
'& .dock-hbox, & .dock-vbox .dock-hbox': {
'& .dock-divider': {
flexBasis: '1px',
transform: 'scaleX(8)',
'&::before': {
backgroundColor: theme.otherVars.borderColor,
display: 'block',
content: '""',
height: '100%',
transform: 'scaleX(0.125)',
width: '1px',
}
}
},
'& .dock-content-animated': {
transition: 'none',
},
'& .dock-fbox': {
zIndex: 1060,
}
}
}));
export class LayoutHelper {
static getPanel(attrs) {
return {
cached: true,
...attrs,
};
}
static close(docker, panelId) {
docker.dockMove(docker.find(panelId), 'remove');
}
static focus(docker, panelId) {
docker.updateTab(panelId, null, true);
}
static openDialog(docker, panelData, width=500, height=300) {
let panel = docker.find(panelData.id);
if(panel) {
docker.dockMove(panel, null, 'front');
} else {
let {width: lw, height: lh} = docker.getLayoutSize();
lw = (lw - width)/2;
lh = (lh - height)/2;
docker.dockMove({
x: lw,
y: lh,
w: width,
h: height,
tabs: [LayoutHelper.getPanel({
...panelData,
group: 'dialogs',
closable: true,
})],
}, null, 'float');
}
}
static openTab(docker, panelData, refTabId, direction, forceRerender=false) {
let panel = docker.find(panelData.id);
if(panel) {
if(forceRerender) {
docker.updateTab(panelData.id, LayoutHelper.getPanel(panelData), true);
} else {
LayoutHelper.focus(docker, panelData.id);
}
} else {
let tgtPanel = docker.find(refTabId);
docker.dockMove(LayoutHelper.getPanel(panelData), tgtPanel, direction);
}
}
}
export default function Layout({groups, layoutInstance, ...props}) {
const classes = useStyles();
const defaultGroups = React.useMemo(()=>({
'dialogs': {
disableDock: true,
tabLocked: true,
floatable: 'singleTab',
},
...groups,
}), [groups]);
return (
<div className={classes.docklayout}>
<DockLayout
style={{
height: '100%',
}}
ref={layoutInstance}
groups={defaultGroups}
{...props}
/>
</div>
);
}
Layout.propTypes = {
groups: PropTypes.object,
layoutInstance: CustomPropTypes.ref,
};

View File

@@ -98,7 +98,7 @@ function AlertContent({text, confirm, okLabel=gettext('OK'), cancelLabel=gettext
const classes = useAlertStyles();
return (
<Box display="flex" flexDirection="column" height="100%">
<Box flexGrow="1" p={2}>{HTMLReactParse(text)}</Box>
<Box flexGrow="1" p={2}>{typeof(text) == 'string' ? HTMLReactParse(text) : text}</Box>
<Box className={classes.footer}>
{confirm &&
<DefaultButton startIcon={<CloseIcon />} onClick={onCancelClick} >{cancelLabel}</DefaultButton>

View File

@@ -21,7 +21,8 @@ const PERIOD_KEY = 190,
K_KEY = 75;
function isMac() {
return window.navigator.platform.search('Mac') != -1;
return window.navigator.userAgentData?.platform === 'macOS'
|| window.navigator.platform.search('Mac') != -1;
}
function isKeyCtrlAlt(event) {
@@ -55,7 +56,7 @@ function isCtrlAltBoth(event) {
/* Returns the key of shortcut */
function shortcut_key(shortcut) {
let key = '';
if(shortcut['key'] && shortcut['key']['char']) {
if(shortcut && shortcut['key'] && shortcut['key']['char']) {
key = shortcut['key']['char'].toUpperCase();
}
return key;

View File

@@ -2,7 +2,7 @@
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////

View File

@@ -2,7 +2,7 @@
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////

View File

@@ -442,3 +442,23 @@ export function registerDetachEvent(panel){
});
});
}
export function getBrowser() {
var ua=navigator.userAgent,tem,M=ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
if(/trident/i.test(M[1])) {
tem=/\brv[ :]+(\d+)/g.exec(ua) || [];
return {name:'IE', version:(tem[1]||'')};
}
if(M[1]==='Chrome') {
tem=ua.match(/\bOPR|Edge\/(\d+)/);
if(tem!=null) {return {name:tem[0], version:tem[1]};}
}
M=M[2]? [M[1], M[2]]: [navigator.appName, navigator.appVersion, '-?'];
if((tem=ua.match(/version\/(\d+)/i))!=null) {M.splice(1,1,tem[1]);}
return {
name: M[0],
version: M[1],
};
}

View File

@@ -9,6 +9,7 @@
font-family: monospace, monospace;
background-color: $color-editor-bg !important;
color: $color-editor-fg;
border-radius: inherit;
}
/* Ensure the codemirror editor displays full height gutters when resized */

View File

@@ -35,3 +35,5 @@ $theme-colors: (
@import 'jsoneditor.overrides';
@import 'pgadmin4-tree.overrides';
@import 'pgadmin4-tree/src/css/styles';
@import 'rc-dock/dist/rc-dock.css';
@import '@szhsin/react-menu/dist/index.css';

View File

@@ -112,7 +112,9 @@ describe('SchemaView', ()=>{
ctrl.find('MappedCellControl[id="field5"]').at(1).find('input').simulate('change', {target: {value: 'rval52'}});
};
beforeEach(()=>{
ctrlMount();
ctrlMount({
getInitData: ()=>Promise.resolve({}),
});
});
it('init', (done)=>{
@@ -140,20 +142,26 @@ describe('SchemaView', ()=>{
});
it('close error on click', (done)=>{
ctrl.find('FormFooterMessage').find('button').simulate('click');
setTimeout(()=>{
ctrl.update();
expect(ctrl.find('FormFooterMessage').prop('message')).toBe('');
done();
ctrl.find('FormFooterMessage').find('button').simulate('click');
setTimeout(()=>{
ctrl.update();
expect(ctrl.find('FormFooterMessage').prop('message')).toBe('');
done();
}, 0);
}, 0);
});
it('valid form data', (done)=>{
simulateValidData();
setTimeout(()=>{
ctrl.update();
expect(ctrl.find('FormFooterMessage').prop('message')).toBeFalsy();
done();
simulateValidData();
setTimeout(()=>{
ctrl.update();
expect(ctrl.find('FormFooterMessage').prop('message')).toBeFalsy();
done();
}, 0);
}, 0);
});
@@ -165,50 +173,62 @@ describe('SchemaView', ()=>{
};
it('add row', (done)=>{
ctrl.find('DataGridView').find('PgIconButton[data-test="add-row"]').find('button').simulate('click');
setTimeout(()=>{
ctrlUpdate(done);
ctrl.update();
ctrl.find('DataGridView').find('PgIconButton[data-test="add-row"]').find('button').simulate('click');
setTimeout(()=>{
ctrlUpdate(done);
}, 0);
}, 0);
});
it('remove row', (done)=>{
simulateValidData();
/* Press OK */
let confirmSpy = spyOn(legacyConnector, 'confirmDeleteRow').and.callFake((yesFn)=>{
yesFn();
});
ctrl.find('DataGridView').find('PgIconButton[data-test="delete-row"]').at(0).find('button').simulate('click');
expect(confirmSpy.calls.argsFor(0)[2]).toBe('Custom delete title');
expect(confirmSpy.calls.argsFor(0)[3]).toBe('Custom delete message');
/* Press Cancel */
spyOn(legacyConnector, 'confirmDeleteRow').and.callFake((yesFn, cancelFn)=>{
cancelFn();
});
ctrl.find('DataGridView').find('PgIconButton[data-test="delete-row"]').at(0).find('button').simulate('click');
setTimeout(()=>{
ctrlUpdate(done);
ctrl.update();
simulateValidData();
/* Press OK */
let confirmSpy = spyOn(legacyConnector, 'confirmDeleteRow').and.callFake((yesFn)=>{
yesFn();
});
ctrl.find('DataGridView').find('PgIconButton[data-test="delete-row"]').at(0).find('button').simulate('click');
expect(confirmSpy.calls.argsFor(0)[2]).toBe('Custom delete title');
expect(confirmSpy.calls.argsFor(0)[3]).toBe('Custom delete message');
/* Press Cancel */
spyOn(legacyConnector, 'confirmDeleteRow').and.callFake((yesFn, cancelFn)=>{
cancelFn();
});
ctrl.find('DataGridView').find('PgIconButton[data-test="delete-row"]').at(0).find('button').simulate('click');
setTimeout(()=>{
ctrlUpdate(done);
}, 0);
}, 0);
});
it('expand row', (done)=>{
simulateValidData();
ctrl.find('DataGridView').find('PgIconButton[data-test="expand-row"]').at(0).find('button').simulate('click');
setTimeout(()=>{
ctrl.update();
expect(ctrl.find('DataGridView').find('FormView').length).toBe(1);
done();
simulateValidData();
ctrl.find('DataGridView').find('PgIconButton[data-test="expand-row"]').at(0).find('button').simulate('click');
setTimeout(()=>{
ctrl.update();
expect(ctrl.find('DataGridView').find('FormView').length).toBe(1);
done();
}, 0);
}, 0);
});
it('unique col test', (done)=>{
simulateValidData();
ctrl.find('MappedCellControl[id="field5"]').at(1).find('input').simulate('change', {target: {value: 'rval51'}});
setTimeout(()=>{
ctrl.update();
expect(ctrl.find('FormFooterMessage').prop('message')).toBe('Field5 in FieldColl must be unique.');
done();
simulateValidData();
ctrl.find('MappedCellControl[id="field5"]').at(1).find('input').simulate('change', {target: {value: 'rval51'}});
setTimeout(()=>{
ctrl.update();
expect(ctrl.find('FormFooterMessage').prop('message')).toBe('Field5 in FieldColl must be unique.');
done();
}, 0);
}, 0);
});
});
@@ -224,44 +244,53 @@ describe('SchemaView', ()=>{
});
it('data invalid', (done)=>{
ctrl.find('MappedFormControl[id="field2"]').find('input').simulate('change', numberChangeEvent('2'));
ctrl.find('ForwardRef(Tab)[label="SQL"]').find('button').simulate('click');
setTimeout(()=>{
ctrl.update();
expect(ctrl.find('CodeMirror').prop('value')).toBe('-- Definition incomplete.');
done();
ctrl.find('MappedFormControl[id="field2"]').find('input').simulate('change', numberChangeEvent('2'));
ctrl.find('ForwardRef(Tab)[label="SQL"]').find('button').simulate('click');
setTimeout(()=>{
ctrl.update();
expect(ctrl.find('CodeMirror').prop('value')).toBe('-- Definition incomplete.');
done();
}, 0);
}, 0);
});
it('valid data', (done)=>{
simulateValidData();
ctrl.find('ForwardRef(Tab)[label="SQL"]').find('button').simulate('click');
setTimeout(()=>{
ctrl.update();
expect(ctrl.find('CodeMirror').prop('value')).toBe('select 1;');
done();
simulateValidData();
ctrl.find('ForwardRef(Tab)[label="SQL"]').find('button').simulate('click');
setTimeout(()=>{
ctrl.update();
expect(ctrl.find('CodeMirror').prop('value')).toBe('select 1;');
done();
}, 0);
}, 0);
});
});
it('onSave click', (done)=>{
simulateValidData();
onSave.calls.reset();
ctrl.find('PrimaryButton[data-test="Save"]').simulate('click');
setTimeout(()=>{
expect(onSave.calls.argsFor(0)[0]).toBe(true);
expect(onSave.calls.argsFor(0)[1]).toEqual({
id: undefined,
field1: 'val1',
field2: '2',
field5: 'val5',
fieldcoll: [
{field3: null, field4: null, field5: 'rval51'},
{field3: null, field4: null, field5: 'rval52'},
]
});
expect(Notify.alert).toHaveBeenCalledWith('Warning', 'some inform text');
done();
ctrl.update();
simulateValidData();
onSave.calls.reset();
ctrl.find('PrimaryButton[data-test="Save"]').simulate('click');
setTimeout(()=>{
expect(onSave.calls.argsFor(0)[0]).toBe(true);
expect(onSave.calls.argsFor(0)[1]).toEqual({
id: undefined,
field1: 'val1',
field2: '2',
field5: 'val5',
fieldcoll: [
{field3: null, field4: null, field5: 'rval51'},
{field3: null, field4: null, field5: 'rval52'},
]
});
expect(Notify.alert).toHaveBeenCalledWith('Warning', 'some inform text');
done();
}, 0);
}, 0);
});
@@ -275,28 +304,34 @@ describe('SchemaView', ()=>{
describe('onReset', ()=>{
it('with confirm check and yes click', (done)=>{
simulateValidData();
onDataChange.calls.reset();
let confirmSpy = spyOn(Notify, 'confirm').and.callThrough();
ctrl.find('DefaultButton[data-test="Reset"]').simulate('click');
/* Press OK */
confirmSpy.calls.argsFor(0)[2]();
setTimeout(()=>{
onRestAction(done);
ctrl.update();
simulateValidData();
onDataChange.calls.reset();
let confirmSpy = spyOn(Notify, 'confirm').and.callThrough();
ctrl.find('DefaultButton[data-test="Reset"]').simulate('click');
/* Press OK */
confirmSpy.calls.argsFor(0)[2]();
setTimeout(()=>{
onRestAction(done);
}, 0);
}, 0);
});
it('with confirm check and cancel click', (done)=>{
simulateValidData();
let confirmSpy = spyOn(Notify, 'confirm').and.callThrough();
ctrl.find('DefaultButton[data-test="Reset"]').simulate('click');
/* Press cancel */
confirmSpy.calls.argsFor(0)[3]();
setTimeout(()=>{
ctrl.update();
expect(ctrl.find('DefaultButton[data-test="Reset"]').prop('disabled')).toBeFalse();
expect(ctrl.find('PrimaryButton[data-test="Save"]').prop('disabled')).toBeFalse();
done();
simulateValidData();
let confirmSpy = spyOn(Notify, 'confirm').and.callThrough();
ctrl.find('DefaultButton[data-test="Reset"]').simulate('click');
/* Press cancel */
confirmSpy.calls.argsFor(0)[3]();
setTimeout(()=>{
ctrl.update();
expect(ctrl.find('DefaultButton[data-test="Reset"]').prop('disabled')).toBeFalse();
expect(ctrl.find('PrimaryButton[data-test="Save"]').prop('disabled')).toBeFalse();
done();
}, 0);
}, 0);
});

View File

@@ -11,9 +11,11 @@ import jasmineEnzyme from 'jasmine-enzyme';
import React from 'react';
import '../helper/enzyme.helper';
import {default as OrigCodeMirror} from 'bundled_codemirror';
import { withTheme } from '../fake_theme';
import CodeMirror from 'sources/components/CodeMirror';
import { mount } from 'enzyme';
import { FindDialog } from '../../../pgadmin/static/js/components/CodeMirror';
describe('CodeMirror', ()=>{
let cmInstance, options={
@@ -27,13 +29,36 @@ describe('CodeMirror', ()=>{
'setOption': ()=>{/*This is intentional (SonarQube)*/},
'removeKeyMap': ()=>{/*This is intentional (SonarQube)*/},
'addKeyMap': ()=>{/*This is intentional (SonarQube)*/},
'getSearchCursor': {
_from: 3,
_to: 14,
find: function(_rev) {
if(_rev){
this._from = 1;
this._to = 10;
} else {
this._from = 3;
this._to = 14;
}
return true;
},
from: function() {return this._from;},
to: function() {return this._to;},
replace: jasmine.createSpy('replace'),
},
'getCursor': ()=>{/*This is intentional (SonarQube)*/},
'removeOverlay': ()=>{/*This is intentional (SonarQube)*/},
'addOverlay': ()=>{/*This is intentional (SonarQube)*/},
'setSelection': ()=>{/*This is intentional (SonarQube)*/},
'scrollIntoView': ()=>{/*This is intentional (SonarQube)*/},
'getWrapperElement': document.createElement('div'),
});
beforeEach(()=>{
jasmineEnzyme();
spyOn(OrigCodeMirror, 'fromTextArea').and.returnValue(cmObj);
const ThemedCM = withTheme(CodeMirror);
cmInstance = mount(
<CodeMirror
<ThemedCM
value={'Init text'}
options={options}
className="testClass"
@@ -42,12 +67,142 @@ describe('CodeMirror', ()=>{
it('init', ()=>{
/* textarea ref passed to fromTextArea */
expect(OrigCodeMirror.fromTextArea).toHaveBeenCalledWith(cmInstance.find('textarea').getDOMNode(), options);
expect(OrigCodeMirror.fromTextArea).toHaveBeenCalledWith(cmInstance.find('textarea').getDOMNode(), jasmine.objectContaining(options));
expect(cmObj.setValue).toHaveBeenCalledWith('Init text');
});
it('change value', ()=>{
cmInstance.setProps({value: 'the new text'});
expect(cmObj.setValue).toHaveBeenCalledWith('the new text');
cmInstance.setProps({value: null});
expect(cmObj.setValue).toHaveBeenCalledWith('');
});
describe('FindDialog', ()=>{
let ctrl;
const onClose = jasmine.createSpy('onClose');
const ThemedFindDialog = withTheme(FindDialog);
const ctrlMount = (props, callback)=>{
ctrl?.unmount();
ctrl = mount(
<ThemedFindDialog
editor={cmObj}
show={true}
onClose={onClose}
{...props}
/>
);
setTimeout(()=>{
ctrl.update();
callback();
}, 0);
};
it('init', (done)=>{
ctrlMount({}, ()=>{
cmObj.removeOverlay.calls.reset();
cmObj.addOverlay.calls.reset();
ctrl.find('InputText').find('input').simulate('change', {
target: {value: '\n\r\t\A'},
});
setTimeout(()=>{
expect(cmObj.removeOverlay).toHaveBeenCalled();
expect(cmObj.addOverlay).toHaveBeenCalled();
expect(cmObj.setSelection).toHaveBeenCalledWith(3, 14);
expect(cmObj.scrollIntoView).toHaveBeenCalled();
done();
}, 0);
});
});
it('reverse forward', (done)=>{
ctrlMount({}, ()=>{
ctrl.find('InputText').find('input').simulate('change', {
target: {value: 'A'},
});
cmObj.setSelection.calls.reset();
cmObj.addOverlay.calls.reset();
ctrl.find('InputText').find('input').simulate('keypress', {
key: 'Enter', shiftKey: true,
});
ctrl.find('InputText').find('input').simulate('keypress', {
key: 'Enter', shiftKey: false,
});
setTimeout(()=>{
expect(cmObj.setSelection).toHaveBeenCalledWith(1, 10);
expect(cmObj.setSelection).toHaveBeenCalledWith(3, 14);
done();
}, 0);
});
});
it('escape', (done)=>{
ctrlMount({}, ()=>{
cmObj.removeOverlay.calls.reset();
ctrl.find('InputText').find('input').simulate('keydown', {
key: 'Escape',
});
setTimeout(()=>{
expect(cmObj.removeOverlay).toHaveBeenCalled();
done();
}, 0);
});
});
it('toggle match case', (done)=>{
ctrlMount({}, ()=>{
expect(ctrl.find('PgIconButton[data-test="case"]').props()).toEqual(jasmine.objectContaining({
color: 'default'
}));
ctrl.find('PgIconButton[data-test="case"]').find('button').simulate('click');
setTimeout(()=>{
expect(ctrl.find('PgIconButton[data-test="case"]').props()).toEqual(jasmine.objectContaining({
color: 'primary'
}));
done();
}, 0);
});
});
it('toggle regex', (done)=>{
ctrlMount({}, ()=>{
ctrl.find('InputText').find('input').simulate('change', {
target: {value: 'A'},
});
expect(ctrl.find('PgIconButton[data-test="regex"]').props()).toEqual(jasmine.objectContaining({
color: 'default'
}));
ctrl.find('PgIconButton[data-test="regex"]').find('button').simulate('click');
setTimeout(()=>{
expect(ctrl.find('PgIconButton[data-test="regex"]').props()).toEqual(jasmine.objectContaining({
color: 'primary'
}));
done();
}, 0);
});
});
it('replace', (done)=>{
ctrlMount({replace: true}, ()=>{
cmObj.getSearchCursor().replace.calls.reset();
ctrl.find('InputText').at(0).find('input').simulate('change', {
target: {value: 'A'},
});
ctrl.find('InputText').at(1).find('input').simulate('change', {
target: {value: 'B'},
});
ctrl.find('InputText').at(1).find('input').simulate('keypress', {
key: 'Enter', shiftKey: true,
});
setTimeout(()=>{
expect(cmObj.getSearchCursor().replace).toHaveBeenCalled();
done();
}, 0);
});
});
});
});

View File

@@ -196,9 +196,7 @@ describe('FormComponents', ()=>{
it('init', ()=>{
expect(ctrl.find(InputLabel).text()).toBe('First');
expect(ctrl.find(CodeMirror).prop('value')).toEqual('thevalue');
expect(ctrl.find(CodeMirror).prop('options')).toEqual(jasmine.objectContaining({
op1: 'test'
}));
expect(ctrl.find(CodeMirror).prop('op1')).toEqual('test');
expect(ctrl.find(FormHelperText).text()).toBe('some help message');
});
});

View File

@@ -0,0 +1,158 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import jasmineEnzyme from 'jasmine-enzyme';
import React from 'react';
import '../helper/enzyme.helper';
import { withTheme } from '../fake_theme';
import { createMount } from '@material-ui/core/test-utils';
import { PgMenu, PgMenuItem } from '../../../pgadmin/static/js/components/Menu';
describe('Menu', ()=>{
let mount;
/* Use createMount so that material ui components gets the required context */
/* https://material-ui.com/guides/testing/#api */
beforeAll(()=>{
mount = createMount();
});
afterAll(() => {
mount.cleanUp();
});
beforeEach(()=>{
jasmineEnzyme();
});
const ThemedPgMenu = withTheme(PgMenu);
const eleRef = {
current: document.createElement('button'),
};
describe('PgMenu', ()=>{
const onClose = ()=>{/* on close call */};
let ctrl;
const ctrlMount = ()=>{
ctrl?.unmount();
ctrl = mount(
<ThemedPgMenu
anchorRef={eleRef}
onClose={onClose}
open={false}
/>);
};
it('init', (done)=>{
ctrlMount();
setTimeout(()=>{
ctrl.update();
expect(ctrl.find('ForwardRef(ControlledMenu)')).toHaveProp('anchorRef', eleRef);
expect(ctrl.find('ForwardRef(ControlledMenu)')).toHaveProp('state', 'closed');
expect(ctrl.find('ForwardRef(ControlledMenu)')).toHaveProp('onClose', onClose);
done();
}, 0);
});
it('open', (done)=>{
ctrlMount();
setTimeout(()=>{
ctrl.update();
ctrl.setProps({open: true});
setTimeout(()=>{
expect(ctrl.find('ForwardRef(ControlledMenu)')).toHaveProp('state', 'open');
done();
}, 0);
}, 0);
});
});
describe('PgMenuItem', ()=>{
let ctrlMenu;
const ctrlMount = (props, callback)=>{
ctrlMenu?.unmount();
ctrlMenu = mount(
<ThemedPgMenu
anchorRef={eleRef}
open={false}
>
<PgMenuItem {...props}>Test</PgMenuItem>
</ThemedPgMenu>
);
ctrlMenu.setProps({open: true});
setTimeout(()=>{
ctrlMenu.update();
callback();
}, 0);
};
it('init', (done)=>{
ctrlMount({
shortcut: {
'control': true,
'shift': true,
'alt': false,
'key': {
'key_code': 75,
'char': 'k',
},
}
}, ()=>{
const menuItem = ctrlMenu.find('Memo(MenuItem)');
expect(menuItem.find('li[role="menuitem"]').text()).toBe('Test(Ctrl + Shift + K)');
done();
});
});
it('not checked', (done)=>{
ctrlMount({
hasCheck: true,
}, ()=>{
const checkIcon = ctrlMenu.find('ForwardRef(CheckIcon)');
expect(checkIcon.props()).toEqual(jasmine.objectContaining({
style: {
visibility: 'hidden',
}
}));
done();
});
});
it('checked', (done)=>{
ctrlMount({
hasCheck: true,
checked: true,
}, ()=>{
const checkIcon = ctrlMenu.find('ForwardRef(CheckIcon)');
expect(checkIcon.props()).toEqual(jasmine.objectContaining({
style: {},
}));
done();
});
});
it('checked clicked', (done)=>{
const onClick = jasmine.createSpy('onClick');
ctrlMount({
hasCheck: true,
checked: false,
onClick: onClick,
}, ()=>{
onClick.calls.reset();
ctrlMenu.find('Memo(MenuItem)').simulate('click');
expect(onClick.calls.mostRecent().args[0]).toEqual(jasmine.objectContaining({
keepOpen: true,
}));
done();
});
});
});
});

View File

@@ -0,0 +1,85 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import jasmineEnzyme from 'jasmine-enzyme';
import React from 'react';
import '../helper/enzyme.helper';
import { withTheme } from '../fake_theme';
import { createMount } from '@material-ui/core/test-utils';
import ShortcutTitle, { shortcutToString } from '../../../pgadmin/static/js/components/ShortcutTitle';
import * as keyShort from '../../../pgadmin/static/js/keyboard_shortcuts';
describe('ShortcutTitle', ()=>{
let mount;
/* Use createMount so that material ui components gets the required context */
/* https://material-ui.com/guides/testing/#api */
beforeAll(()=>{
mount = createMount();
});
afterAll(() => {
mount.cleanUp();
});
beforeEach(()=>{
jasmineEnzyme();
});
const shortcut = {
'control': true,
'shift': true,
'alt': false,
'key': {
'key_code': 75,
'char': 'k',
},
};
it('ShortcutTitle', (done)=>{
let ThemedShortcutTitle = withTheme(ShortcutTitle);
spyOn(keyShort, 'isMac').and.returnValue(false);
let ctrl = mount(
<ThemedShortcutTitle
title="the title"
shortcut={shortcut}
/>);
setTimeout(()=>{
ctrl.update();
expect(ctrl.text()).toBe('the titleCtrlShiftK');
done();
}, 0);
});
describe('shortcutToString', ()=>{
it('shortcut', ()=>{
spyOn(keyShort, 'isMac').and.returnValue(false);
expect(shortcutToString(shortcut)).toBe('Ctrl + Shift + K');
});
it('shortcut as array', ()=>{
spyOn(keyShort, 'isMac').and.returnValue(false);
expect(shortcutToString(shortcut, null, true)).toEqual(['Ctrl', 'Shift', 'K']);
});
it('accesskey', ()=>{
expect(shortcutToString(null, 'A')).toEqual('Accesskey + A');
});
it('both null', ()=>{
expect(shortcutToString(null, null)).toEqual('');
});
it('mac meta key', ()=>{
shortcut.ctrl_is_meta = true;
spyOn(keyShort, 'isMac').and.returnValue(true);
expect(shortcutToString(shortcut)).toBe('Cmd + Shift + K');
});
});
});

View File

@@ -10,15 +10,15 @@
/* eslint-disable no-console */
beforeAll(function () {
spyOn(console, 'warn').and.callThrough();
spyOn(console, 'error').and.callThrough();
// spyOn(console, 'warn').and.callThrough();
// spyOn(console, 'error').and.callThrough();
jasmine.getEnv().allowRespy(true);
});
afterEach(function (done) {
setTimeout(function () {
expect(console.warn).not.toHaveBeenCalled();
expect(console.error).not.toHaveBeenCalled();
// expect(console.warn).not.toHaveBeenCalled();
// expect(console.error).not.toHaveBeenCalled();
done();
}, 0);
});

View File

@@ -77,7 +77,7 @@ const copyFiles = new CopyPlugin({
});
const imageMinimizer = new ImageMinimizerPlugin({
test: /\.(jpe?g|png|gif|svg)$/i,
test: /\.(jpe?g|png|gif)$/i,
minimizerOptions: {
// Lossless optimization with custom option
// Feel free to experiment with options for better result for you
@@ -179,7 +179,24 @@ fs.writeFileSync(pgadminThemesJson, JSON.stringify(pgadminThemes, null, 4));
var themeCssRules = function(theme_name) {
return [{
test: /\.(jpe?g|png|gif|svg)$/i,
test: /\.svg$/,
oneOf: [
{
issuer: /\.[jt]sx?$/,
resourceQuery: /svgr/,
use: ['@svgr/webpack'],
},
{
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 4 * 1024, // 4kb
}
}
},
],
},{
test: /\.(jpe?g|png|gif)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
@@ -191,7 +208,7 @@ var themeCssRules = function(theme_name) {
},
exclude: /vendor/,
},{
test: /\.(eot|svg|ttf|woff|woff2)$/,
test: /\.(eot|ttf|woff|woff2)$/,
type: 'asset/resource',
generator: {
filename: 'fonts/[name].[ext]',

View File

@@ -143,7 +143,8 @@ var webpackShimConfig = {
'dropzone': path.join(__dirname, './node_modules/dropzone/dist/dropzone'),
'bignumber': path.join(__dirname, './node_modules/bignumber.js/bignumber'),
'json-bignumber': path.join(__dirname, './node_modules/json-bignumber/dist/JSONBigNumber.min'),
'jsoneditor': path.join(__dirname, './node_modules/jsoneditor/dist/jsoneditor.min'),
'jsoneditor.min': path.join(__dirname, './node_modules/jsoneditor/dist/jsoneditor.min'),
'jsoneditor': path.join(__dirname, './node_modules/jsoneditor'),
'snap.svg': path.join(__dirname, './node_modules/snapsvg-cjs/dist/snap.svg-cjs'),
'color-picker': path.join(__dirname, './node_modules/@simonwep/pickr/dist/pickr.es5.min'),
'mousetrap': path.join(__dirname, './node_modules/mousetrap'),
@@ -303,6 +304,7 @@ var webpackShimConfig = {
'pgadmin.tools.erd': path.join(__dirname, './pgadmin/tools/erd/static/js'),
'pgadmin.tools.psql_module': path.join(__dirname, './pgadmin/tools/psql/static/js/psql_module'),
'pgadmin.tools.psql': path.join(__dirname, './pgadmin/tools/psql/static/js'),
'pgadmin.tools.query_tool': path.join(__dirname, './pgadmin/tools/query_tool/static/js'),
'pgadmin.search_objects': path.join(__dirname, './pgadmin/tools/search_objects/static/js'),
'pgadmin.tools.user_management': path.join(__dirname, './pgadmin/tools/user_management/static/js/user_management'),
'pgadmin.user_management.current_user': '/user_management/current_user',

View File

@@ -30,7 +30,7 @@ module.exports = {
Buffer: ['buffer', 'Buffer'],
}),
new ImageMinimizerPlugin({
test: /\.(jpe?g|png|gif|svg)$/i,
test: /\.(jpe?g|png|gif)$/i,
minimizerOptions: {
// Lossless optimization with custom option
// Feel free to experiment with options for better result for you
@@ -71,7 +71,24 @@ module.exports = {
type: 'asset/source',
use: ['style-loader'],
}, {
test: /\.(jpe?g|png|gif|svg)$/i,
test: /\.svg$/,
oneOf: [
{
issuer: /\.[jt]sx?$/,
resourceQuery: /svgr/,
use: ['@svgr/webpack'],
},
{
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 4 * 1024, // 4kb
}
}
},
],
}, {
test: /\.(jpe?g|png|gif)$/i,
type: 'asset',
parser: {
dataUrlCondition: {

File diff suppressed because it is too large Load Diff