Implemented utilities in React to make porting easier for pgAdmin tools.
@@ -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",
|
||||
|
@@ -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 */
|
||||
|
1
web/pgadmin/static/img/cleaning_services_black.svg
Normal 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 |
1
web/pgadmin/static/img/content_paste.svg
Normal 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 |
1
web/pgadmin/static/img/filter_alt_black.svg
Normal 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 |
@@ -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 |
1
web/pgadmin/static/img/fonticon/format_case.svg
Normal 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 |
1
web/pgadmin/static/img/fonticon/regex.svg
Normal 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 |
@@ -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 |
@@ -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)=>{
|
||||
|
@@ -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} />;
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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)=>({
|
||||
|
@@ -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: {
|
||||
|
@@ -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',
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
34
web/pgadmin/static/js/components/ExternalIcon.jsx
Normal 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} />;
|
||||
|
@@ -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) => {
|
||||
|
60
web/pgadmin/static/js/components/JsonEditor.jsx
Normal 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,
|
||||
};
|
@@ -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]),
|
||||
};
|
||||
|
83
web/pgadmin/static/js/components/Menu.jsx
Normal 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,
|
||||
};
|
63
web/pgadmin/static/js/components/ShortcutTitle.jsx
Normal 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,
|
||||
};
|
@@ -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}>
|
||||
|
@@ -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]);
|
||||
}
|
||||
|
@@ -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;
|
||||
|
44
web/pgadmin/static/js/helpers/EventBus.js
Normal 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());
|
179
web/pgadmin/static/js/helpers/Layout.jsx
Normal 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,
|
||||
};
|
@@ -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>
|
||||
|
@@ -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;
|
||||
|
@@ -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
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
@@ -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
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
@@ -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],
|
||||
};
|
||||
}
|
||||
|
@@ -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 */
|
||||
|
@@ -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';
|
||||
|
@@ -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);
|
||||
});
|
||||
|
||||
|
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
@@ -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');
|
||||
});
|
||||
});
|
||||
|
158
web/regression/javascript/components/Menu.spec.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
85
web/regression/javascript/components/ShortcutTitle.spec.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
@@ -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);
|
||||
});
|
||||
|
@@ -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]',
|
||||
|
@@ -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',
|
||||
|
@@ -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: {
|
||||
|