1) Port schema diff to React. Fixes #6133

2) Remove SlickGrid.
pull/5349/head
Nikhil Mohite 2022-09-07 19:20:03 +05:30 committed by Akshay Joshi
parent ad59380676
commit e1942d8c9e
78 changed files with 2794 additions and 7888 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 431 KiB

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 KiB

After

Width:  |  Height:  |  Size: 199 KiB

View File

@ -19,6 +19,7 @@ New features
Housekeeping
************
| `Issue #6133 <https://redmine.postgresql.org/issues/6133>`_ - Port schema diff to React.
| `Issue #7343 <https://redmine.postgresql.org/issues/7343>`_ - Port the remaining components of the ERD Tool to React.
| `Issue #7622 <https://redmine.postgresql.org/issues/7622>`_ - Port search object dialog to React.

View File

@ -33,7 +33,6 @@ module.exports = function (config) {
}),
],
files: [
'pgadmin/static/bundle/slickgrid.js',
{pattern: 'pgadmin/static/**/*.js', included: false, watched: true},
{pattern: 'pgadmin/browser/static/js/**/*.js', included: false, watched: true},
{pattern: 'regression/javascript/**/*.js', watched: true},
@ -50,7 +49,6 @@ module.exports = function (config) {
preprocessors: {
'pgadmin/**/js/**/*.js?': ['sourcemap'],
'regression/javascript/**/*.js': ['webpack', 'sourcemap'],
'pgadmin/static/bundle/slickgrid.js': ['webpack', 'sourcemap'],
},
// optionally, configure the reporter

View File

@ -163,7 +163,6 @@
"react-window": "^1.8.5",
"select2": "^4.0.13",
"shim-loader": "^1.0.1",
"slickgrid": "git+https://github.com/6pac/SlickGrid.git#2.3.16",
"snapsvg-cjs": "^0.0.6",
"socket.io-client": "^4.0.0",
"split.js": "^1.5.10",

View File

@ -1,4 +1,4 @@
{
{
"dark": {
"disp_name": "dark_(Beta)",
"cssfile": "pgadmin.theme.dark",

View File

@ -18,7 +18,7 @@ define([
pgAdmin.Browser.Frame = function(options) {
var defaults = [
'name', 'title', 'width', 'height', 'showTitle', 'isCloseable',
'isPrivate', 'url', 'icon', 'onCreate', 'isLayoutMember',
'isPrivate', 'url', 'icon', 'onCreate', 'isLayoutMember', 'isRenamable',
];
_.extend(this, _.pick(options, defaults));
};

View File

@ -11,7 +11,7 @@
{% block init_script %}
try {
require(
['sources/generated/app.bundle', 'sources/generated/codemirror', 'sources/generated/browser_nodes', 'sources/generated/slickgrid'],
['sources/generated/app.bundle', 'sources/generated/codemirror', 'sources/generated/browser_nodes'],
function() {
},
function() {

View File

@ -1,25 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import 'slickgrid/lib/jquery.event.drag-2.3.0';
import 'slickgrid/slick.core';
import 'slickgrid/slick.grid';
import 'slickgrid/slick.dataview';
import 'slickgrid/slick.editors';
import 'slickgrid/slick.formatters';
import 'slickgrid/slick.groupitemmetadataprovider';
import 'slickgrid/plugins/slick.autotooltips';
import 'slickgrid/plugins/slick.cellrangedecorator';
import 'slickgrid/plugins/slick.cellrangeselector';
import 'slickgrid/plugins/slick.checkboxselectcolumn';
import 'slickgrid/plugins/slick.rowselectionmodel';
import 'sources/slickgrid/custom_header_buttons';
import 'sources/slickgrid/plugins/slick.autocolumnsize';
export default window.Slick;

View File

@ -14,10 +14,6 @@
@import 'node_modules/codemirror/addon/dialog/dialog.css';
@import 'node_modules/codemirror/addon/scroll/simplescrollbars.css';
@import 'node_modules/slickgrid/slick.grid.css';
@import 'node_modules/slickgrid/slick-default-theme.css';
@import 'node_modules/slickgrid/css/smoothness/jquery-ui-1.11.3.custom.css';
@import '../vendor/backgrid/backgrid.css';
@import '../vendor/backgrid/backgrid-select-all.css';

View File

@ -0,0 +1,115 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import _ from 'lodash';
import React, { useState, useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Box } from '@material-ui/core';
import CloseIcon from '@material-ui/icons/CloseRounded';
import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
import pgAdmin from 'sources/pgadmin';
import gettext from 'sources/gettext';
import { DefaultButton, PrimaryButton } from '../components/Buttons';
import { useModalStyles } from '../helpers/ModalProvider';
import { FormFooterMessage, InputText, MESSAGE_TYPE } from '../components/FormComponents';
import { setDebuggerTitle } from '../../../tools/debugger/static/js/debugger_utils';
import * as queryToolPanelTitleFunc from '../../../tools/sqleditor/static/js/sqleditor_title';
import * as queryToolPanelViewDataFunc from '../../../tools/sqleditor/static/js/show_view_data';
export default function RenamePanelContent({ closeModal, panel, title, preferences, setHeight, tabType, data }) {
const classes = useModalStyles();
const containerRef = useRef();
const firstEleRef = useRef();
const okBtnRef = useRef();
const [formData, setFormData] = useState({
title: title
});
const onTextChange = (e, id) => {
let val = e;
if (e && e.target) {
val = e.target.value;
}
setFormData((prev) => ({ ...prev, [id]: val }));
};
const onKeyDown = (e) => {
// If enter key is pressed then click on OK button
if (e.key === 'Enter') {
okBtnRef.current?.click();
}
};
useEffect(() => {
setTimeout(() => {
firstEleRef.current && firstEleRef.current.focus();
}, 350);
}, [firstEleRef.current]);
useEffect(() => {
setHeight?.(containerRef.current?.offsetHeight);
}, [containerRef.current, formData]);
return (
<Box display="flex" flexDirection="column" className={classes.container} ref={containerRef}>
<Box flexGrow="1" p={2}>
<Box marginTop='12px'>
<InputText inputRef={firstEleRef} type="text" value={formData['title']} controlProps={{ maxLength: null }}
onChange={(e) => onTextChange(e, 'title')} onKeyDown={(e) => onKeyDown(e)} />
</Box>
<FormFooterMessage type={MESSAGE_TYPE.ERROR} message={formData['title'].length == 0 ? gettext('Title cannot be empty') : ''} closable={false} style={{
position: 'unset', padding: '12px 0px 0px'
}} />
</Box>
<Box className={classes.footer}>
<DefaultButton data-test="close" startIcon={<CloseIcon />} onClick={() => {
closeModal();
}} >{gettext('Cancel')}</DefaultButton>
<PrimaryButton ref={okBtnRef} data-test="save" className={classes.margin} disabled={formData['title'].length == 0 ? true : false} startIcon={<CheckRoundedIcon />} onClick={() => {
if (tabType == 'debugger') {
setDebuggerTitle(panel, preferences, data.function_name, data.schema_name, data.database_name, _.escape(formData['title']), pgAdmin.Browser);
} else if (tabType == 'querytool') {
let selected_item = pgAdmin.Browser.tree.selected();
let panel_titles = '';
if (data.is_query_tool) {
panel_titles = queryToolPanelTitleFunc.getPanelTitle(pgAdmin.Browser, selected_item, formData['title']);
} else {
panel_titles = queryToolPanelViewDataFunc.generateViewDataTitle(pgAdmin.Browser, selected_item, formData['title']);
}
// Set title to the selected tab.
if (data.is_dirty_editor) {
panel_titles = panel_titles + ' *';
}
queryToolPanelTitleFunc.setQueryToolDockerTitle(panel, data.is_query_tool, panel_titles, data.is_file);
} else {
panel.title(_.escape(formData.title));
}
closeModal();
}} >{gettext('OK')}</PrimaryButton>
</Box>
</Box>
);
}
RenamePanelContent.propTypes = {
closeModal: PropTypes.func,
data: PropTypes.object,
setHeight: PropTypes.func,
panel: PropTypes.object,
title: PropTypes.string,
preferences: PropTypes.object,
tabType: PropTypes.string,
'panel.title': PropTypes.string,
};

View File

@ -22,6 +22,7 @@ import ChangePasswordContent from './ChangePasswordContent';
import NamedRestoreContent from './NamedRestoreContent';
import ChangeOwnershipContent from './ChangeOwnershipContent';
import UrlDialogContent from './UrlDialogContent';
import RenamePanelContent from './RenamePanelContent';
function mountDialog(title, getDialogContent, docker=undefined, width, height) {
// Register dialog panel
@ -353,3 +354,29 @@ export function showUrlDialog() {
{ isFullScreen: false, isResizeable: true, showFullScreen: true, isFullWidth: true,
dialogWidth: width || pgAdmin.Browser.stdW.md, dialogHeight: height || pgAdmin.Browser.stdH.md});
}
export function showRenamePanel() {
let title = arguments[0],
preferences = arguments[1],
panel = arguments[2],
tabType= arguments[3],
data= arguments[4];
mountDialog(gettext(`Rename Panel ${_.escape(title)}`), (onClose, setNewSize)=> {
return <Theme>
<RenamePanelContent
setHeight={(containerHeight)=>{
setNewSize(pgAdmin.Browser.stdW.md, containerHeight);
}}
closeModal={()=>{
onClose();
}}
panel={panel}
tabType={tabType}
title={title}
data={data}
preferences={preferences}
/>
</Theme>;
});
}

View File

@ -103,6 +103,13 @@ export default function(basicSettings) {
textMuted: '#8A8A8A',
erdCanvasBg: '#303030',
erdGridColor: '#444952',
schemaDiff: {
diffRowColor: '#807a48',
sourceRowColor: '#402025',
targetRowColor: '#6b5438',
diffColorFg: '#d4d4d4',
diffSelectFG: '#d4d4d4'
}
}
});
}

View File

@ -101,6 +101,13 @@ export default function(basicSettings) {
textMuted: '#8b9cad',
erdCanvasBg: '#010B15',
erdGridColor: '#1F2932',
schemaDiff: {
diffRowColor: '#CFC56E',
sourceRowColor: '#EE97A5',
targetRowColor: '#FFAD65',
diffColorFg: '#FFFFFF',
diffSelectFG: '#010B15'
}
}
});
}

View File

@ -121,6 +121,13 @@ export default function(basicSettings) {
color: '#FFFFFF',
bg: '#880000'
},
},
schemaDiff: {
diffRowColor: '#fff9c4',
sourceRowColor: '#ffebee',
targetRowColor: '#fbe3bf',
diffColorFg: '#222',
diffSelectFG: '#222'
}
}
});

View File

@ -37,6 +37,7 @@ const useStyles = makeStyles((theme)=>({
},
message: {
marginLeft: '0.5rem',
fontSize: '16px',
}
}));

View File

@ -128,7 +128,7 @@ CustomRow.propTypes = {
};
export default function PgReactDataGrid({gridRef, className, hasSelectColumn=true, onItemEnter, onItemSelect,
onItemClick, noRowsText, ...props}) {
onItemClick, noRowsText, noRowsIcon,...props}) {
const classes = useStyles();
let finalClassName = [classes.root];
hasSelectColumn && finalClassName.push(classes.hasSelectColumn);
@ -142,7 +142,7 @@ export default function PgReactDataGrid({gridRef, className, hasSelectColumn=tru
components={{
sortIcon: CutomSortIcon,
rowRenderer: CustomRow,
noRowsFallback: <Box textAlign="center" gridColumn="1/-1" p={1}>{noRowsText || gettext('No rows found.')}</Box>,
noRowsFallback: <Box textAlign="center" gridColumn="1/-1" p={1}>{noRowsIcon}{noRowsText || gettext('No rows found.')}</Box>,
}}
{...props}
/>
@ -158,5 +158,6 @@ PgReactDataGrid.propTypes = {
onItemEnter: PropTypes.func,
onItemSelect: PropTypes.func,
onItemClick: PropTypes.func,
noRowsText: PropTypes.string
noRowsText: PropTypes.string,
noRowsIcon: PropTypes.elementType
};

View File

@ -67,6 +67,12 @@ const useStyles = makeStyles((theme)=>({
'& .dock-tabpane': {
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
},
'& #id-schema-diff': {
overflowY: 'auto'
},
'& #id-results': {
overflowY: 'auto'
}
},
'& .dock-tab': {

View File

@ -1,203 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
define([
'jquery',
'sources/selection/range_selection_helper',
], function ($, RangeSelectionHelper) {
return function () {
var KEY_RIGHT = 39;
var KEY_LEFT = 37;
var KEY_UP = 38;
var KEY_DOWN = 40;
var bypassDefaultActiveCellRangeChange = false;
var isColumnsResized = false;
var isMouseInHeader = false;
var grid;
var init = function (slickGrid) {
grid = slickGrid;
grid.onDragEnd.subscribe(onDragEndHandler);
grid.onHeaderClick.subscribe(onHeaderClickHandler);
grid.onClick.subscribe(onClickHandler);
grid.onActiveCellChanged.subscribe(onActiveCellChangedHandler);
grid.onKeyDown.subscribe(onKeyDownHandler);
grid.onHeaderMouseEnter.subscribe(onHeaderMouseEnterHandler);
grid.onHeaderMouseLeave.subscribe(onHeaderMouseLeaveHandler);
grid.onColumnsResized.subscribe(onColumnsResizedHandler);
};
var destroy = function () {
grid.onDragEnd.unsubscribe(onDragEndHandler);
grid.onHeaderClick.unsubscribe(onHeaderClickHandler);
grid.onActiveCellChanged.unsubscribe(onActiveCellChangedHandler);
grid.onKeyDown.unsubscribe(onKeyDownHandler);
grid.onHeaderMouseEnter.unsubscribe(onHeaderMouseEnterHandler);
grid.onHeaderMouseLeave.unsubscribe(onHeaderMouseLeaveHandler);
grid.onColumnsResized.unsubscribe(onColumnsResizedHandler);
};
$.extend(this, {
'init': init,
'destroy': destroy,
});
function onDragEndHandler(event, dragData) {
bypassDefaultActiveCellRangeChange = true;
grid.setActiveCell(dragData.range.start.row, dragData.range.start.cell);
}
function onHeaderClickHandler(event, args) {
if (isColumnsResizedAndCurrentlyInHeader()) {
isColumnsResized = false;
event.stopPropagation();
return;
}
/* Skip if clicked on resize handler */
if($(event.target).hasClass('slick-resizable-handle')) {
return;
}
bypassDefaultActiveCellRangeChange = true;
var clickedColumn = args.column.pos + 1;
if (isClickingLastClickedHeader(0, clickedColumn)) {
if (isSingleRangeSelected()) {
grid.resetActiveCell();
} else {
grid.setActiveCell(0, retrievePreviousSelectedRange().fromCell);
}
} else if (!isClickingInSelectedColumn(clickedColumn)) {
grid.setActiveCell(0, clickedColumn);
}
}
function isEditableNewRow(row) {
return row >= grid.getDataLength();
}
function onHeaderMouseLeaveHandler() {
isMouseInHeader = false;
}
function onHeaderMouseEnterHandler() {
isMouseInHeader = true;
isColumnsResized = false;
}
function onColumnsResizedHandler() {
isColumnsResized = true;
}
function onClickHandler(event, args) {
if (isRowHeader(args.cell)) {
bypassDefaultActiveCellRangeChange = true;
var rowClicked = args.row;
if (isEditableNewRow(rowClicked)) {
return;
}
if (isClickingLastClickedHeader(rowClicked, 1)) {
if (isSingleRangeSelected() || grid.getSelectionModel().getSelectedRanges().length === 0) {
grid.resetActiveCell();
} else {
grid.setActiveCell(retrievePreviousSelectedRange().fromRow, 1);
}
} else if (!isClickingInSelectedRow(rowClicked)) {
grid.setActiveCell(rowClicked, 1);
}
}
}
function onActiveCellChangedHandler(event) {
if (bypassDefaultActiveCellRangeChange) {
bypassDefaultActiveCellRangeChange = false;
event.stopPropagation();
}
}
function onKeyDownHandler(event) {
if (hasActiveCell() && isShiftArrowKey(event)) {
selectOnlyRangeOfActiveCell();
}
}
function isColumnsResizedAndCurrentlyInHeader() {
return isMouseInHeader && isColumnsResized;
}
function isClickingLastClickedHeader(clickedRow, clickedColumn) {
return hasActiveCell() && grid.getActiveCell().row === clickedRow && grid.getActiveCell().cell === clickedColumn;
}
function isClickingInSelectedColumn(clickedColumn) {
var column = RangeSelectionHelper.rangeForColumn(grid, clickedColumn);
var cellSelectionModel = grid.getSelectionModel();
var ranges = cellSelectionModel.getSelectedRanges();
return RangeSelectionHelper.isRangeSelected(ranges, column);
}
function isRowHeader(cellClicked) {
return grid.getColumns()[cellClicked].id === 'row-header-column';
}
function isClickingInSelectedRow(rowClicked) {
var row = RangeSelectionHelper.rangeForRow(grid, rowClicked);
var cellSelectionModel = grid.getSelectionModel();
var ranges = cellSelectionModel.getSelectedRanges();
return RangeSelectionHelper.isRangeSelected(ranges, row);
}
function isSingleRangeSelected() {
var cellSelectionModel = grid.getSelectionModel();
var ranges = cellSelectionModel.getSelectedRanges();
return ranges.length === 1;
}
function retrievePreviousSelectedRange() {
var cellSelectionModel = grid.getSelectionModel();
var ranges = cellSelectionModel.getSelectedRanges();
return ranges[ranges.length - 2];
}
function isArrowKey(event) {
return event.which === KEY_RIGHT
|| event.which === KEY_UP
|| event.which === KEY_LEFT
|| event.which === KEY_DOWN;
}
function isModifiedByShiftOnly(event) {
return event.shiftKey
&& !event.ctrlKey
&& !event.altKey;
}
function isShiftArrowKey(event) {
return isModifiedByShiftOnly(event) && isArrowKey(event);
}
function hasActiveCell() {
return !!grid.getActiveCell();
}
function selectOnlyRangeOfActiveCell() {
var cellSelectionModel = grid.getSelectionModel();
var ranges = cellSelectionModel.getSelectedRanges();
if (ranges.length > 1) {
cellSelectionModel.setSelectedRanges([ranges.pop()]);
}
}
};
});

View File

@ -1,164 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
define([
'jquery',
'sources/selection/range_selection_helper',
'slickgrid',
], function ($, RangeSelectionHelper) {
return function () {
var Slick = window.Slick,
gridEventBus = new Slick.EventHandler(),
onBeforeColumnSelectAll = new Slick.Event(),
onColumnSelectAll = new Slick.Event();
var init = function (grid) {
gridEventBus.subscribe(grid.onHeaderClick, handleHeaderClick.bind(null, grid));
grid.getSelectionModel().onSelectedRangesChanged
.subscribe(handleSelectedRangesChanged.bind(null, grid));
onColumnSelectAll.subscribe(function(e, args) {
updateRanges(args.grid, args.column.id);
});
};
var handleHeaderClick = function (grid, event, args) {
var columnDefinition = args.column;
grid.focus();
if (isColumnSelectable(columnDefinition)) {
var $columnHeader = $(event.target);
if (hasClickedChildOfColumnHeader(event)) {
if ($(event.target).hasClass('slick-resizable-handle')) {
return;
}
$columnHeader = $(event.target).parents('.slick-header-column');
}
$columnHeader.toggleClass('selected');
if ($columnHeader.hasClass('selected')) {
onBeforeColumnSelectAll.notify(args, event);
}
if (!(event.isPropagationStopped() || event.isImmediatePropagationStopped())) {
updateRanges(grid, columnDefinition.id);
}
} else {
toggleColumnHeaderForCopyHeader(grid);
}
};
var toggleColumnHeaderForCopyHeader = function(grid) {
if(!$('.copy-with-header').hasClass('visibility-hidden')) {
var selRowCnt = grid.getSelectedRows();
$('.slick-header-column').each(function (index, columnHeader) {
if (selRowCnt.length == 0) {
$(columnHeader).removeClass('selected');
grid.getColumns()[index].selected = false;
}
else {
if (index > 0 && grid.getColumns()[index].selectable) {
$(columnHeader).addClass('selected');
grid.getColumns()[index].selected = true;
}
}
});
} else {
$('.slick-header-column').each(function (index, columnHeader) {
$(columnHeader).removeClass('selected');
});
}
}.bind(RangeSelectionHelper);
var handleSelectedRangesChanged = function (grid, event, selectedRanges) {
$('.slick-header-column').each(function (index, columnHeader) {
var $spanHeaderColumn = $(columnHeader).find('[data-cell-type="column-header-row"]');
var columnIndex = grid.getColumnIndex($spanHeaderColumn.data('column-id'));
if (isColumnSelected(grid, selectedRanges, columnIndex)) {
$(columnHeader).addClass('selected');
if (columnIndex) grid.getColumns()[columnIndex].selected = true;
} else if(!RangeSelectionHelper.areAllRangesCompleteRows(grid, selectedRanges)){
$(columnHeader).removeClass('selected');
if (columnIndex) grid.getColumns()[columnIndex].selected = false;
}
});
};
var updateRanges = function (grid, columnId) {
var selectionModel = grid.getSelectionModel();
var ranges = selectionModel.getSelectedRanges();
var columnIndex = grid.getColumnIndex(columnId);
var columnRange = RangeSelectionHelper.rangeForColumn(grid, columnIndex);
var newRanges;
if (RangeSelectionHelper.isRangeSelected(ranges, columnRange)) {
newRanges = RangeSelectionHelper.removeRange(ranges, columnRange);
} else {
if (RangeSelectionHelper.areAllRangesSingleColumns(ranges, grid)) {
newRanges = RangeSelectionHelper.addRange(ranges, columnRange);
} else {
newRanges = [columnRange];
}
}
selectionModel.setSelectedRanges(newRanges);
};
var hasClickedChildOfColumnHeader = function (event) {
return !$(event.target).hasClass('slick-header-column');
};
var isColumnSelectable = function (columnDefinition) {
return columnDefinition.selectable !== false;
};
var isColumnSelected = function (grid, selectedRanges, columnIndex) {
var allRangesAreRows = RangeSelectionHelper.areAllRangesCompleteRows(grid, selectedRanges);
return isAnyCellSelectedInColumn(grid, selectedRanges, columnIndex) && !allRangesAreRows;
};
var isAnyCellSelectedInColumn = function (grid, selectedRanges, columnIndex) {
var isStillSelected = RangeSelectionHelper.isRangeEntirelyWithinSelectedRanges(selectedRanges,
RangeSelectionHelper.rangeForColumn(grid, columnIndex));
var cellSelectedInColumn = RangeSelectionHelper.isAnyCellOfColumnSelected(selectedRanges, columnIndex);
return isStillSelected || cellSelectedInColumn;
};
var getColumnDefinitions = function (columnDefinitions) {
return _.map(columnDefinitions, function (columnDefinition) {
if (isColumnSelectable(columnDefinition)) {
var name =
'<span data-cell-type=\'column-header-row\' ' +
' data-test=\'output-column-header\'' +
' data-column-id=\'' + columnDefinition.id + '\'>' +
' <span class=\'column-description\'>' +
' <span class=\'column-name\'>' + columnDefinition.display_name + '</span>' +
' <span class=\'column-type\'>' + columnDefinition.column_type + '</span>' +
' </span>' +
'</span>';
return _.extend(columnDefinition, {
name: name,
});
} else {
return columnDefinition;
}
});
};
$.extend(this, {
'init': init,
'getColumnDefinitions': getColumnDefinitions,
'onBeforeColumnSelectAll': onBeforeColumnSelectAll,
'onColumnSelectAll': onColumnSelectAll,
'toggleColumnHeaderForCopyHeader': toggleColumnHeaderForCopyHeader,
});
};
});

View File

@ -1,58 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
define([
'jquery',
'underscore',
'sources/selection/clipboard',
'sources/selection/range_selection_helper',
'sources/selection/range_boundary_navigator',
],
function ($, _, clipboard, RangeSelectionHelper, rangeBoundaryNavigator) {
var copyData = function () {
var self = this || window;
var grid = self.slickgrid;
var columnDefinitions = grid.getColumns();
var selectedRanges = grid.getSelectionModel().getSelectedRanges();
var dataView = grid.getData();
var rows = grid.getSelectedRows();
var CSVOptions = grid.CSVOptions;
if (RangeSelectionHelper.areAllRangesCompleteRows(grid, selectedRanges)) {
self.copied_rows = rows.map(function (rowIndex) {
return grid.getDataItem(rowIndex);
});
setPasteRowButtonEnablement(self.can_edit, true);
} else {
self.copied_rows = [];
setPasteRowButtonEnablement(self.can_edit, false);
}
var csvText = rangeBoundaryNavigator.rangesToCsv(dataView.getItems(), columnDefinitions,
selectedRanges, CSVOptions, copyWithHeader());
if (csvText) {
clipboard.copyTextToClipboard(csvText);
}
};
var copyWithHeader = function () {
return !$('.copy-with-header').hasClass('visibility-hidden');
};
var setPasteRowButtonEnablement = function (canEditFlag, isEnabled) {
if (canEditFlag) {
$('#btn-paste-row').prop('disabled', !isEnabled);
if(isEnabled && window.parent.$) {
// trigger copied event to all sessions
window.parent.$(window.parent.document).trigger('pgadmin-sqleditor:rows-copied', copyWithHeader());
}
}
};
return copyData;
});

View File

@ -1,92 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
define(['jquery',
'sources/gettext',
'sources/selection/column_selector',
'sources/selection/row_selector',
'sources/selection/range_selection_helper',
'sources/url_for',
], function ($, gettext, ColumnSelector, RowSelector, RangeSelectionHelper, url_for) {
return function (columnDefinitions) {
var Slick = window.Slick,
rowSelector = new RowSelector(columnDefinitions),
columnSelector = new ColumnSelector(columnDefinitions),
onBeforeGridSelectAll = new Slick.Event(),
onGridSelectAll = new Slick.Event(),
onBeforeGridColumnSelectAll = columnSelector.onBeforeColumnSelectAll,
onGridColumnSelectAll = columnSelector.onColumnSelectAll;
var init = function (grid) {
this.grid = grid;
grid.onHeaderClick.subscribe(function (event, eventArguments) {
if (eventArguments.column.selectAllOnClick && !$(event.target).hasClass('slick-resizable-handle')) {
toggleSelectAll(grid, event, eventArguments);
}
});
grid.getSelectionModel().onSelectedRangesChanged
.subscribe(handleSelectedRangesChanged.bind(null, grid));
grid.registerPlugin(rowSelector);
grid.registerPlugin(columnSelector);
onGridSelectAll.subscribe(function(e, args) {
RangeSelectionHelper.selectAll(args.grid);
});
};
var getColumnDefinitions = function (columnDefinition) {
columnDefinition = columnSelector.getColumnDefinitions(columnDefinition);
columnDefinition = rowSelector.getColumnDefinitions(columnDefinition);
columnDefinition[0].selectAllOnClick = true;
columnDefinition[0].name = '<span data-id="select-all" ' +
'title="' + gettext('Select/Deselect All') + '">' +
'<br>' +
columnDefinition[0].name +
'<img class="select-all-icon" src="' + url_for('static', {'filename': 'img/select-all-icon.png'}) + '"></img>' +
'</span>';
return columnDefinition;
};
function handleSelectedRangesChanged(grid) {
if(RangeSelectionHelper.isEntireGridSelected(grid)) {
$('[data-id=\'select-all\']').addClass('selected');
} else {
$('[data-id=\'select-all\']').removeClass('selected');
}
}
function toggleSelectAll(grid, event, eventArguments) {
if (RangeSelectionHelper.isEntireGridSelected(grid)) {
selectNone(grid);
} else {
onBeforeGridSelectAll.notify(eventArguments, event);
if (!(event.isPropagationStopped() || event.isImmediatePropagationStopped())) {
RangeSelectionHelper.selectAll(grid);
}
}
}
function selectNone(grid) {
var selectionModel = grid.getSelectionModel();
selectionModel.setSelectedRanges([]);
}
$.extend(this, {
'init': init,
'getColumnDefinitions': getColumnDefinitions,
'onBeforeGridSelectAll': onBeforeGridSelectAll,
'onGridSelectAll': onGridSelectAll,
'onBeforeGridColumnSelectAll': onBeforeGridColumnSelectAll,
'onGridColumnSelectAll': onGridColumnSelectAll,
});
};
});

View File

@ -1,171 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
define(['slickgrid'], function () {
var Slick = window.Slick;
var isSameRange = function (range, otherRange) {
return range.fromCell == otherRange.fromCell && range.toCell == otherRange.toCell &&
range.fromRow == otherRange.fromRow && range.toRow == otherRange.toRow;
};
var isRangeSelected = function (selectedRanges, range) {
return _.any(selectedRanges, function (selectedRange) {
return isSameRange(selectedRange, range);
});
};
var isAnyCellOfColumnSelected = function (selectedRanges, column) {
return _.any(selectedRanges, function (selectedRange) {
return selectedRange.fromCell <= column && selectedRange.toCell >= column;
});
};
var isAnyCellOfRowSelected = function (selectedRanges, row) {
return _.any(selectedRanges, function (selectedRange) {
return selectedRange.fromRow <= row && selectedRange.toRow >= row;
});
};
var isRangeEntirelyWithinSelectedRanges = function (selectedRanges, range) {
return _.any(selectedRanges, function (selectedRange) {
return selectedRange.fromCell <= range.fromCell && selectedRange.toCell >= range.toCell &&
selectedRange.fromRow <= range.fromRow && selectedRange.toRow >= range.toRow;
});
};
var removeRange = function (selectedRanges, range) {
return _.filter(selectedRanges, function (selectedRange) {
return !(isSameRange(selectedRange, range));
});
};
var addRange = function (ranges, range) {
ranges.push(range);
return ranges;
};
var areAllRangesSingleRows = function (ranges, grid) {
return _.every(ranges, function (range) {
return range.fromRow == range.toRow && rangeHasCompleteRows(grid, range);
});
};
var areAllRangesSingleColumns = function (ranges, grid) {
return _.every(ranges, isRangeAColumn.bind(this, grid));
};
var rangeForRow = function (grid, rowId) {
var columnDefinitions = grid.getColumns();
if (isFirstColumnData(columnDefinitions)) {
return new Slick.Range(rowId, 0, rowId, grid.getColumns().length - 1);
}
return new Slick.Range(rowId, 1, rowId, grid.getColumns().length - 1);
};
var rangeForColumn = function (grid, columnIndex) {
return new Slick.Range(0, columnIndex, grid.getDataLength() - 1, columnIndex);
};
var getRangeOfWholeGrid = function (grid) {
return new Slick.Range(0, 1, grid.getDataLength() - 1, grid.getColumns().length - 1);
};
var isEntireGridSelected = function (grid) {
var selectionModel = grid.getSelectionModel();
var selectedRanges = selectionModel.getSelectedRanges();
return selectedRanges.length == 1 && isSameRange(selectedRanges[0], getRangeOfWholeGrid(grid));
};
var isFirstColumnData = function (columnDefinitions) {
return !_.isUndefined(columnDefinitions[0].pos);
};
var areAllRangesCompleteColumns = function (grid, ranges) {
return _.every(ranges, function (range) {
return rangeHasCompleteColumns(grid, range);
});
};
var areAllRangesCompleteRows = function (grid, ranges) {
return _.every(ranges, function (range) {
return rangeHasCompleteRows(grid, range);
});
};
var getIndexesOfCompleteRows = function (grid, ranges) {
var indexArray = [];
ranges.forEach(function (range) {
if (rangeHasCompleteRows(grid, range))
indexArray = indexArray.concat(_.range(range.fromRow, range.toRow + 1));
});
return indexArray;
};
var isRangeAColumn = function (grid, range) {
return range.fromCell == range.toCell &&
range.fromRow == 0 && range.toRow == grid.getDataLength() - 1;
};
var rangeHasCompleteColumns = function (grid, range) {
return range.fromRow === 0 && range.toRow === grid.getDataLength() - 1;
};
var rangeHasCompleteRows = function (grid, range) {
return range.fromCell === getFirstDataColumnIndex(grid) &&
range.toCell === getLastDataColumnIndex(grid);
};
function getFirstDataColumnIndex(grid) {
return _.findIndex(grid.getColumns(), function (columnDefinition) {
var pos = columnDefinition.pos;
return !_.isUndefined(pos) && isSelectable(columnDefinition);
});
}
function getLastDataColumnIndex(grid) {
return _.findLastIndex(grid.getColumns(), isSelectable);
}
function isSelectable(columnDefinition) {
return (_.isUndefined(columnDefinition.selectable) || columnDefinition.selectable === true);
}
function selectAll(grid) {
var range = getRangeOfWholeGrid(grid);
var selectionModel = grid.getSelectionModel();
selectionModel.setSelectedRanges([range]);
}
return {
addRange: addRange,
removeRange: removeRange,
isRangeSelected: isRangeSelected,
areAllRangesSingleRows: areAllRangesSingleRows,
areAllRangesSingleColumns: areAllRangesSingleColumns,
areAllRangesCompleteRows: areAllRangesCompleteRows,
areAllRangesCompleteColumns: areAllRangesCompleteColumns,
rangeForRow: rangeForRow,
rangeForColumn: rangeForColumn,
isEntireGridSelected: isEntireGridSelected,
getRangeOfWholeGrid: getRangeOfWholeGrid,
isFirstColumnData: isFirstColumnData,
getIndexesOfCompleteRows: getIndexesOfCompleteRows,
selectAll: selectAll,
isRangeAColumn: isRangeAColumn,
rangeHasCompleteColumns: rangeHasCompleteColumns,
rangeHasCompleteRows: rangeHasCompleteRows,
isAnyCellOfColumnSelected: isAnyCellOfColumnSelected,
isRangeEntirelyWithinSelectedRanges: isRangeEntirelyWithinSelectedRanges,
isAnyCellOfRowSelected: isAnyCellOfRowSelected,
};
});

View File

@ -1,110 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
define([
'jquery',
'sources/selection/range_selection_helper',
'sources/selection/column_selector',
'slickgrid',
], function ($, RangeSelectionHelper, ColumnSelector) {
return function () {
var Slick = window.Slick;
var gridEventBus = new Slick.EventHandler();
var columnSelector = new ColumnSelector();
var init = function (grid) {
grid.getSelectionModel().onSelectedRangesChanged
.subscribe(handleSelectedRangesChanged.bind(null, grid));
gridEventBus
.subscribe(grid.onClick, handleClick.bind(null, grid));
};
var handleClick = function (grid, event, args) {
if (grid.getColumns()[args.cell].id === 'row-header-column') {
var $rowHeaderSpan = $(event.target);
if ($rowHeaderSpan.data('cell-type') != 'row-header-selector') {
$rowHeaderSpan = $(event.target).find('[data-cell-type="row-header-selector"]');
}
$rowHeaderSpan.parent().toggleClass('selected');
updateRanges(grid, args.row);
columnSelector.toggleColumnHeaderForCopyHeader(grid);
}
};
var handleSelectedRangesChanged = function (grid, event, selectedRanges) {
$('[data-cell-type="row-header-selector"]').each(function (index, rowHeaderSpan) {
var $rowHeaderSpan = $(rowHeaderSpan);
var row = parseInt($rowHeaderSpan.data('row'));
if (isRowSelected(grid, selectedRanges, row)) {
$rowHeaderSpan.parent().addClass('selected');
} else {
$rowHeaderSpan.parent().removeClass('selected');
}
});
};
var updateRanges = function (grid, rowId) {
var selectionModel = grid.getSelectionModel();
var ranges = selectionModel.getSelectedRanges();
var rowRange = RangeSelectionHelper.rangeForRow(grid, rowId);
var newRanges;
if (RangeSelectionHelper.isRangeSelected(ranges, rowRange)) {
newRanges = RangeSelectionHelper.removeRange(ranges, rowRange);
} else {
if (RangeSelectionHelper.areAllRangesSingleRows(ranges, grid)) {
newRanges = RangeSelectionHelper.addRange(ranges, rowRange);
} else {
newRanges = [rowRange];
}
}
selectionModel.setSelectedRanges(newRanges);
};
var isAnyCellSelectedInRow = function (grid, selectedRanges, row) {
var isStillSelected = RangeSelectionHelper.isRangeEntirelyWithinSelectedRanges(selectedRanges,
RangeSelectionHelper.rangeForRow(grid, row));
var cellSelectedInRow = RangeSelectionHelper.isAnyCellOfRowSelected(selectedRanges, row);
return isStillSelected || cellSelectedInRow;
};
var isRowSelected = function (grid, selectedRanges, row) {
var allRangesAreColumns = RangeSelectionHelper.areAllRangesCompleteColumns(grid, selectedRanges);
return isAnyCellSelectedInRow(grid, selectedRanges, row) && !allRangesAreColumns;
};
var getColumnDefinitions = function (columnDefinitions) {
columnDefinitions.unshift({
id: 'row-header-column',
name: '',
selectable: false,
focusable: false,
formatter: function (rowIndex) {
return '<span ' +
'data-row="' + rowIndex + '" ' +
'data-cell-type="row-header-selector">' +
(rowIndex+1) + '</span>';
},
width: 30,
});
return columnDefinitions;
};
$.extend(this, {
'init': init,
'getColumnDefinitions': getColumnDefinitions,
});
};
});

View File

@ -1,240 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
define([
'jquery',
'underscore',
'sources/selection/range_selection_helper',
'sources/window',
'slickgrid',
], function ($, _, RangeSelectionHelper, pgWindow) {
return function (options) {
var KEY_ARROW_RIGHT = 39;
var KEY_ARROW_LEFT = 37;
var KEY_ARROW_UP = 38;
var KEY_ARROW_DOWN = 40;
var Slick = window.Slick;
var _grid;
var _ranges = [];
var _self = this;
var _selector = new Slick.CellRangeSelector({
selectionCss: {
border: '2px solid black',
},
offset: {
top: 0,
left: -1,
height: 2,
width: 1,
},
});
var _options;
var _defaults = {
selectActiveCell: true,
};
function init(grid) {
_options = $.extend(true, {}, _defaults, options);
_grid = grid;
_grid.onActiveCellChanged.subscribe(handleActiveCellChange);
_grid.onKeyDown.subscribe(handleKeyDown);
grid.registerPlugin(_selector);
_selector.onCellRangeSelected.subscribe(handleCellRangeSelected);
_selector.onBeforeCellRangeSelected.subscribe(handleBeforeCellRangeSelected);
$(pgWindow.default).on('mouseup',handleWindowMouseUp);
}
function destroy() {
_grid.onActiveCellChanged.unsubscribe(handleActiveCellChange);
_grid.onKeyDown.unsubscribe(handleKeyDown);
_selector.onCellRangeSelected.unsubscribe(handleCellRangeSelected);
_selector.onBeforeCellRangeSelected.unsubscribe(handleBeforeCellRangeSelected);
_grid.unregisterPlugin(_selector);
$(pgWindow.default).off('mouseup', handleWindowMouseUp);
}
function removeInvalidRanges(ranges) {
var result = [];
for (let range_val of ranges) {
var r = range_val;
if (_grid.canCellBeSelected(r.fromRow, r.fromCell) && _grid.canCellBeSelected(r.toRow, r.toCell)) {
result.push(r);
}
}
return result;
}
function setSelectedRanges(ranges) {
// simple check for: empty selection didn't change, prevent firing onSelectedRangesChanged
if ((!_ranges || _ranges.length === 0) && (!ranges || ranges.length === 0)) { return; }
_ranges = removeInvalidRanges(ranges);
_self.onSelectedRangesChanged.notify(_ranges);
}
function getSelectedRanges() {
return _ranges;
}
function setSelectedRows(rows) {
_ranges = [];
for(let row_val of rows) {
_ranges.push(RangeSelectionHelper.rangeForRow(_grid, row_val));
}
}
function handleBeforeCellRangeSelected(e) {
if (_grid.getEditorLock().isActive()) {
e.stopPropagation();
return false;
}
}
function handleCellRangeSelected(e, args) {
setSelectedRanges([args.range]);
}
function handleActiveCellChange(e, args) {
if (_options.selectActiveCell && args.row != null && args.cell != null) {
setSelectedRanges([new Slick.Range(args.row, args.cell)]);
}
}
function arrowKeyPressed(event) {
return event.which == KEY_ARROW_RIGHT
|| event.which == KEY_ARROW_LEFT
|| event.which == KEY_ARROW_UP
|| event.which == KEY_ARROW_DOWN;
}
function shiftArrowKeyPressed(event) {
return event.shiftKey && !event.ctrlKey && !event.altKey &&
(arrowKeyPressed(event));
}
function needUpdateRange(newRange) {
return removeInvalidRanges([newRange]).length;
}
function handleKeyDown(e) {
var ranges;
var lastSelectedRange;
var anchorActiveCell = _grid.getActiveCell();
function isKey(key) { return e.which === key; }
function getKeycode() { return e.which; }
function shouldScrollToBottommostRow() { return anchorActiveCell.row === newSelectedRange.fromRow; }
function shouldScrollToRightmostColumn() { return anchorActiveCell.cell === newSelectedRange.fromCell; }
function getMobileCellFromRange(range, activeCell) {
var localMobileCell = {};
localMobileCell.row = range.fromRow === activeCell.row ? range.toRow : range.fromRow;
localMobileCell.cell = range.fromCell === activeCell.cell ? range.toCell : range.fromCell;
return localMobileCell;
}
function getNewRange(rangeCorner, oppositeCorner) {
var newFromCell = rangeCorner.cell <= oppositeCorner.cell ? rangeCorner.cell : oppositeCorner.cell;
var newToCell = rangeCorner.cell <= oppositeCorner.cell ? oppositeCorner.cell : rangeCorner.cell;
var newFromRow = rangeCorner.row <= oppositeCorner.row ? rangeCorner.row : oppositeCorner.row;
var newToRow = rangeCorner.row <= oppositeCorner.row ? oppositeCorner.row : rangeCorner.row;
return new Slick.Range(
newFromRow,
newFromCell,
newToRow,
newToCell
);
}
if (anchorActiveCell && shiftArrowKeyPressed(e)) {
ranges = getSelectedRanges();
if (!ranges.length) {
ranges.push(new Slick.Range(anchorActiveCell.row, anchorActiveCell.cell));
}
// keyboard can work with last range only
lastSelectedRange = ranges.pop();
// can't handle selection out of active cell
if (!lastSelectedRange.contains(anchorActiveCell.row, anchorActiveCell.cell)) {
lastSelectedRange = new Slick.Range(anchorActiveCell.row, anchorActiveCell.cell);
}
var mobileCell = getMobileCellFromRange(lastSelectedRange, anchorActiveCell);
switch (getKeycode()) {
case KEY_ARROW_LEFT:
mobileCell.cell -= 1;
break;
case KEY_ARROW_RIGHT:
mobileCell.cell += 1;
break;
case KEY_ARROW_UP:
mobileCell.row -= 1;
break;
case KEY_ARROW_DOWN:
mobileCell.row += 1;
break;
}
var newSelectedRange = getNewRange(anchorActiveCell, mobileCell);
if (needUpdateRange(newSelectedRange)) {
var rowToView = shouldScrollToBottommostRow() ? newSelectedRange.toRow : newSelectedRange.fromRow;
var columnToView = shouldScrollToRightmostColumn() ? newSelectedRange.toCell : newSelectedRange.fromCell;
if (isKey(KEY_ARROW_RIGHT) || isKey(KEY_ARROW_LEFT)) {
_grid.scrollColumnIntoView(columnToView);
} else if (isKey(KEY_ARROW_UP) || isKey(KEY_ARROW_DOWN)) {
_grid.scrollRowIntoView(rowToView);
}
ranges.push(newSelectedRange);
} else {
ranges.push(lastSelectedRange);
}
setSelectedRanges(ranges);
e.preventDefault();
e.stopPropagation();
}
}
function handleWindowMouseUp() {
var selectedRange = _selector.getCurrentRange();
if (!_.isUndefined(selectedRange)) {
_grid.onDragEnd.notify({range: selectedRange});
}
}
$.extend(this, {
'getSelectedRanges': getSelectedRanges,
'setSelectedRanges': setSelectedRanges,
'setSelectedRows': setSelectedRows,
'init': init,
'destroy': destroy,
'onSelectedRangesChanged': new Slick.Event(),
});
};
});

View File

@ -1,27 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
define(['slickgrid'], function () {
var Slick = window.Slick;
return function () {
this.init = function (grid) {
grid.onActiveCellChanged.subscribe(function (event, slickEvent) {
grid.getSelectionModel().setSelectedRanges([
new Slick.Range(
slickEvent.row,
slickEvent.cell,
slickEvent.row,
slickEvent.cell
),
]);
});
};
};
});

View File

@ -1,189 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
(function ($) {
// register namespace
$.extend(true, window, {
'Slick': {
'Plugins': {
'HeaderButtons': HeaderButtons,
},
},
});
/***
* custom header button modified from slick.headerbuttons.js
*
* USAGE:
*
* Add the plugin .js & .css files and register it with the grid.
*
* To specify a custom button in a column header, extend the column definition like so:
*
* var columns = [
* {
* id: 'myColumn',
* name: 'My column',
*
* // This is the relevant part
* header: {
* buttons: [
* {
* // button options
* },
* {
* // button options
* }
* ]
* }
* }
* ];
*
* Available button options:
* cssClass: CSS class to add to the button.
* image: Relative button image path.
* tooltip: Button tooltip.
* showOnHover: Only show the button on hover.
* handler: Button click handler.
* command: A command identifier to be passed to the onCommand event handlers.
*
* The plugin exposes the following events:
* onCommand: Fired on button click for buttons with 'command' specified.
* Event args:
* grid: Reference to the grid.
* column: Column definition.
* command: Button command identified.
* button: Button options. Note that you can change the button options in your
* event handler, and the column header will be automatically updated to
* reflect them. This is useful if you want to implement something like a
* toggle button.
*
*
* @param options {Object} Options:
* buttonCssClass: a CSS class to use for buttons (default 'slick-header-button')
* @class Slick.Plugins.HeaderButtons
* @constructor
*/
function HeaderButtons(options) {
var _grid;
var _self = this;
var _handler = new window.Slick.EventHandler();
var _defaults = {
buttonCssClass: 'slick-header-button',
};
function init(grid) {
options = $.extend(true, {}, _defaults, options);
_grid = grid;
_handler
.subscribe(_grid.onHeaderCellRendered, handleHeaderCellRendered)
.subscribe(_grid.onBeforeHeaderCellDestroy, handleBeforeHeaderCellDestroy);
// Force the grid to re-render the header now that the events are hooked up.
_grid.setColumns(_grid.getColumns());
}
function destroy() {
_handler.unsubscribeAll();
}
function handleHeaderCellRendered(e, args) {
var column = args.column;
if (column.header && column.header.buttons) {
// Append buttons in reverse order since they are floated to the right.
var i = column.header.buttons.length;
while (i--) {
var button = column.header.buttons[i];
var btn = $('<div></div>')
.addClass(options.buttonCssClass)
.data('column', column)
.data('button', button);
if (button.content){
btn.append(button.content);
}
if (button.showOnHover) {
btn.addClass('slick-header-button-hidden');
}
if (button.image) {
btn.css('backgroundImage', 'url(' + button.image + ')');
}
if (button.cssClass) {
btn.addClass(button.cssClass);
}
if (button.tooltip) {
btn.attr('title', button.tooltip);
}
if (button.command) {
btn.data('command', button.command);
}
if (button.handler) {
btn.bind('click', button.handler);
}
btn
.bind('click', handleButtonClick)
.prependTo(args.node);
}
}
}
function handleBeforeHeaderCellDestroy(e, args) {
var column = args.column;
if (column.header && column.header.buttons) {
// Removing buttons via jQuery will also clean up any event handlers and data.
// NOTE: If you attach event handlers directly or using a different framework,
// you must also clean them up here to avoid memory leaks.
$(args.node).find('.' + options.buttonCssClass).remove();
}
}
function handleButtonClick(e) {
var command = $(this).data('command');
var columnDef = $(this).data('column');
var button = $(this).data('button');
if (command != null) {
_self.onCommand.notify({
'grid': _grid,
'column': columnDef,
'command': command,
'button': button,
}, e, _self);
// Update the header in case the user updated the button definition in the handler.
_grid.updateColumnHeader(columnDef.id);
}
// Stop propagation so that it doesn't register as a header click event.
e.preventDefault();
e.stopPropagation();
}
$.extend(this, {
'init': init,
'destroy': destroy,
'onCommand': new window.Slick.Event(),
});
}
})(window.jQuery);

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
define([
'sources/selection/copy_data',
'sources/selection/range_selection_helper',
],
function (copyData, RangeSelectionHelper) {
return function handleQueryOutputKeyboardEvent(event, args) {
var KEY_C = 67;
var KEY_A = 65;
var modifiedKey = event.keyCode;
var isModifierDown = event.ctrlKey || event.metaKey;
var self = this || window;
self.slickgrid = args.grid;
if (isModifierDown && modifiedKey == KEY_C) {
copyData.apply(self);
}
if (isModifierDown && modifiedKey == KEY_A) {
RangeSelectionHelper.selectAll(self.slickgrid);
}
};
});

View File

@ -1,132 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
/***
* Contains pgAdmin4 related SlickGrid formatters.
*
* @module Formatters
* @namespace Slick
*/
(function($) {
// register namespace
$.extend(true, window, {
'Slick': {
'Formatters': {
'JsonString': JsonFormatter,
'Numbers': NumbersFormatter,
'Checkmark': CheckmarkFormatter,
'Text': TextFormatter,
'Binary': BinaryFormatter,
},
},
});
function NullAndDefaultFormatter(row, cell, value, columnDef) {
if (_.isUndefined(value) && columnDef.has_default_val) {
return '<span class=\'pull-left disabled_cell\'>[default]</span>';
} else if (
(_.isUndefined(value) && columnDef.not_null) ||
(_.isUndefined(value) || value === null)
) {
return '<span class=\'pull-left disabled_cell\'>[null]</span>';
}
return null;
}
function NullAndDefaultNumberFormatter(row, cell, value, columnDef) {
if (_.isUndefined(value) && columnDef.has_default_val) {
return '<span class=\'pull-right disabled_cell\'>[default]</span>';
} else if (
(_.isUndefined(value) && columnDef.not_null) ||
(_.isUndefined(value) || value === null)
) {
return '<span class=\'pull-right disabled_cell\'>[null]</span>';
}
return null;
}
function JsonFormatter(row, cell, value, columnDef) {
// If column has default value, set placeholder
var data = NullAndDefaultFormatter(row, cell, value, columnDef);
if (data) {
return data;
} else {
// Stringify only if it's json object
if (typeof value === 'object' && !Array.isArray(value)) {
return _.escape(JSON.stringify(value));
} else if (Array.isArray(value)) {
var temp = [];
$.each(value, function(i, val) {
if (typeof val === 'object') {
temp.push(JSON.stringify(val));
} else {
temp.push(val);
}
});
return _.escape('[' + temp.join() + ']');
} else {
return _.escape(value);
}
}
}
function NumbersFormatter(row, cell, value, columnDef) {
// If column has default value, set placeholder
var data = NullAndDefaultNumberFormatter(row, cell, value, columnDef);
if (data) {
return data;
} else {
return '<span style=\'float:right\'>' + _.escape(value) + '</span>';
}
}
function CheckmarkFormatter(row, cell, value, columnDef) {
/* Checkbox has 3 states
* 1) checked=true
* 2) unchecked=false
* 3) indeterminate=null
*/
var data = NullAndDefaultFormatter(row, cell, value, columnDef);
if (data) {
return data;
} else {
return value ? 'true' : 'false';
}
}
function TextFormatter(row, cell, value, columnDef) {
// If column has default value, set placeholder
var raw = NullAndDefaultFormatter(row, cell, value, columnDef);
var data;
if (raw) {
data = raw;
} else {
data = _.escape(value);
}
// Replace leading whitespace with a marker so we don't just show blank lines
if (data.trimStart() != data) {
data = '[...] ' + data.trimStart();
}
return data;
}
function BinaryFormatter(row, cell, value, columnDef) {
// If column has default value, set placeholder
var data = NullAndDefaultFormatter(row, cell, value, columnDef);
if (data) {
return data;
} else {
return '<span class=\'pull-left disabled_cell\'>[' + _.escape(value) + ']</span>';
}
}
})(window.jQuery);

View File

@ -1,169 +0,0 @@
/*
* https://github.com/naresh-n/slickgrid-column-data-autosize
*/
(function($) {
$.extend(true, window, {
'Slick': {
'AutoColumnSize': AutoColumnSize,
},
});
function AutoColumnSize() {
var grid, $container, context,
keyCodes = {
'A': 65,
};
function init(_grid) {
grid = _grid;
$container = $(grid.getContainerNode());
$container.on('dblclick.autosize', '.slick-resizable-handle', reSizeColumn);
$container.keydown(handleControlKeys);
context = document.createElement('canvas').getContext('2d');
// Expose resizeAllColumns method to call from outside of this file.
grid.resizeAllColumns = resizeAllColumns;
}
function destroy() {
$container.off();
}
function handleControlKeys(event) {
if (event.ctrlKey && event.shiftKey && event.keyCode === keyCodes.A) {
resizeAllColumns();
}
}
function resizeAllColumns(maxWidth, max_width_changed=false) {
var elHeaders = $container.find('.slick-header-column');
var allColumns = grid.getColumns();
elHeaders.each(function(index, el) {
var columnDef = $(el).data('column');
// Check if width is set then no need to resize that column.
if (typeof(columnDef.width) !== 'undefined' && !isNaN(columnDef.width) && !max_width_changed) {
return;
}
var headerWidth = getElementWidth(el);
var colIndex = grid.getColumnIndex(columnDef.id);
var column = allColumns[colIndex];
var autoSizeWidth = Math.max(headerWidth, getMaxColumnTextWidth(columnDef, colIndex)) + 1;
// If max width is provided and it is greater than 0
if (typeof(maxWidth) !== 'undefined' && maxWidth > 0) {
autoSizeWidth = Math.min(maxWidth, autoSizeWidth);
}
column.width = autoSizeWidth + (columnDef.addWidth || 0);
});
grid.setColumns(allColumns);
grid.onColumnsResized.notify();
}
function reSizeColumn(e) {
var headerEl = $(e.currentTarget).closest('.slick-header-column');
var columnDef = headerEl.data('column');
if (!columnDef || !columnDef.resizable) {
return;
}
e.preventDefault();
e.stopPropagation();
var headerWidth = getElementWidth(headerEl[0]);
var colIndex = grid.getColumnIndex(columnDef.id);
var allColumns = grid.getColumns();
var column = allColumns[colIndex];
var autoSizeWidth = Math.max(headerWidth, getMaxColumnTextWidth(columnDef, colIndex)) + 1;
if (autoSizeWidth !== column.width) {
column.width = autoSizeWidth;
grid.setColumns(allColumns);
grid.onColumnsResized.notify();
}
}
function getMaxColumnTextWidth(columnDef, colIndex) {
var texts = [];
var rowEl = createRow();
var data = grid.getData();
if (window.Slick.Data && data instanceof window.Slick.Data.DataView) {
data = data.getItems();
}
for (let data_val of data) {
texts.push(data_val[columnDef.field]);
}
var template = getMaxTextTemplate(texts, columnDef, colIndex, data, rowEl);
var width = getTemplateWidth(rowEl, template);
deleteRow(rowEl);
return width;
}
function getTemplateWidth(rowEl, template) {
var cell = $(rowEl.find('.slick-cell'));
cell.append(template);
cell.find('*').css('position', 'relative');
return cell.outerWidth() + 1;
}
function getMaxTextTemplate(texts, columnDef, colIndex, data, rowEl) {
var max = 0,
maxTemplate = null;
var formatFun = columnDef.formatter;
$(texts).each(function(index, text) {
var template;
if (formatFun) {
template = $('<span>' + formatFun(index, colIndex, text, columnDef, data[index]) + '</span>');
text = template.text() || text;
}
var length = text ? getElementWidthUsingCanvas(rowEl, text) : 0;
if (length > max) {
max = length;
maxTemplate = template || text;
}
});
return maxTemplate;
}
function createRow() {
var rowEl = $('<div class="slick-row"><div class="slick-cell"></div></div>');
rowEl.find('.slick-cell').css({
'visibility': 'hidden',
'text-overflow': 'initial',
'white-space': 'nowrap',
});
var gridCanvas = $container.find('.grid-canvas').first();
$(gridCanvas).append(rowEl);
return rowEl;
}
function deleteRow(rowEl) {
$(rowEl).remove();
}
function getElementWidth(element) {
var width, clone = element.cloneNode(true);
clone.style.cssText = 'position: absolute; visibility: hidden;right: auto;text-overflow: initial;white-space: nowrap;';
element.parentNode.insertBefore(clone, element);
width = clone.offsetWidth;
clone.parentNode.removeChild(clone);
return width;
}
function getElementWidthUsingCanvas(element, text) {
context.font = element.css('font-size') + ' ' + element.css('font-family');
var metrics = context.measureText(text);
return metrics.width;
}
return {
init: init,
destroy: destroy,
};
}
}(window.jQuery));

View File

@ -833,18 +833,6 @@ body {
font-size: inherit !important;
}
/* CSS for custom checkbox editor in SlickGrid */
.multi-checkbox .check {
display: inline-block;
vertical-align: top;
width: 16px;
height: 16px;
border: 1px solid $color-gray-dark;
margin: 3px;
text-align: center;
line-height: 16px;
}
.multi-checkbox .check.checked,
.multi-checkbox .check.unchecked {
background: $color-bg;

View File

@ -1,83 +0,0 @@
.slick-row .cell-actions {
text-align: left;
}
.slick-row.selected .cell-selection {
background-color: transparent; /* show default selected row background */
}
.slick-cell span[data-cell-type="row-header-selector"] {
display: block;
text-align: center;
}
/*
SlickGrid, To fix the issue of width misalignment between Column Header &
actual Column in Mozilla Firefox browser
Ref: https://github.com/mleibman/SlickGrid/issues/742
*/
.slickgrid, .slickgrid *, .slick-header-column {
box-sizing: content-box;
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box;
-ms-box-sizing: content-box;
}
.slick-cell.selected span[data-cell-type="row-header-selector"] {
color: $color-primary-fg;
}
.slick-cell.cell-move-handle {
font-weight: bold;
text-align: right;
border-right: solid $border-color;
background: $color-gray-lighter;
cursor: move;
&:hover {
background: $color-gray-light;
}
}
.cell-selection {
border-right-color: $border-color;
border-right-style: solid;
background: $color-gray-lighter;
color: $color-gray;
text-align: right;
font-size: 10px;
}
.slick-row.selected .cell-move-handle {
background: $color-warning-light;
}
.slick-row.complete {
background-color: $color-success-light;
color: $color-gray-dark;
}
.slick-row:hover .slick-cell{
border-top: $table-hover-border;
border-bottom: $table-hover-border;
background-color: $table-hover-bg-color;
}
.slick-row .slick-cell {
border-bottom: $panel-border;
border-right: $panel-border;
z-index: 0;
}
.slick-cell.active {
border: 1px solid transparent;
border-right: 1px solid $color-gray-light;
border-bottom-color: $color-gray-light;
}
.ui-widget-content.slick-row {
&.even, &.odd {
background: none;
background-color: $table-bg;
}
}

View File

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

View File

@ -16,7 +16,6 @@ import gettext from 'sources/gettext';
import { sprintf, registerDetachEvent } from 'sources/utils';
import url_for from 'sources/url_for';
import pgWindow from 'sources/window';
import alertify from 'pgadmin.alertifyjs';
import Kerberos from 'pgadmin.authenticate.kerberos';
import { refresh_db_node } from 'tools/sqleditor/static/js/sqleditor_title';
@ -28,6 +27,7 @@ import FunctionArguments from './debugger_ui';
import ModalProvider from '../../../../static/js/helpers/ModalProvider';
import DebuggerComponent from './components/DebuggerComponent';
import Theme from '../../../../static/js/Theme';
import { showRenamePanel } from '../../../../static/js/Dialogs';
export default class DebuggerModule {
static instance;
@ -713,20 +713,13 @@ export default class DebuggerModule {
}
panel_rename_event(panel_data, panel, treeInfo) {
alertify.prompt('', panel_data.$titleText[0].textContent,
// We will execute this function when user clicks on the OK button
function (evt, value) {
if (value) {
// Remove the leading and trailing white spaces.
value = value.trim();
let preferences = this.pgBrowser.get_preferences_for_module('browser');
var name = getAppropriateLabel(treeInfo);
setDebuggerTitle(panel, preferences, name, treeInfo.schema.label, treeInfo.database.label, value, this.pgBrowser);
}
},
// We will execute this function when user clicks on the Cancel
// button. Do nothing just close it.
function (evt) { evt.cancel = false; }
).set({ 'title': gettext('Rename Panel') });
let name = getAppropriateLabel(treeInfo);
let preferences = this.pgBrowser.get_preferences_for_module('browser');
let data = {
function_name: name,
schema_name: treeInfo.schema.label,
database_name: treeInfo.database.label
};
showRenamePanel(panel_data.$titleText[0].textContent, preferences, panel, 'debugger', data);
}
}

View File

@ -22,7 +22,6 @@ import gettext from 'sources/gettext';
import * as commonUtils from 'sources/utils';
import pgAdmin from 'sources/pgadmin';
import Loader from 'sources/components/Loader';
import Alertify from 'pgadmin.alertifyjs';
import SchemaView from '../../../../../static/js/SchemaView';
import getApiInstance from '../../../../../static/js/api_instance';
@ -31,7 +30,7 @@ import { getAppropriateLabel, setDebuggerTitle } from '../debugger_utils';
import Notify from '../../../../../static/js/helpers/Notifier';
import { DebuggerArgumentSchema } from './DebuggerArgs.ui';
import { DEBUGGER_ARGS } from '../DebuggerConstants';
import { showRenamePanel } from '../../../../../static/js/Dialogs';
const useStyles = makeStyles((theme) =>
@ -677,7 +676,6 @@ export default function DebuggerArgumentComponent({ debuggerInfo, restartDebug,
}
function startDebugging() {
var self = this;
setLoaderText('Starting debugger.');
try {
/* Initialize the target once the debug button is clicked and create asynchronous connection
@ -759,20 +757,7 @@ export default function DebuggerArgumentComponent({ debuggerInfo, restartDebug,
// Panel Rename event
panel.on(wcDocker.EVENT.RENAME, function (panel_data) {
Alertify.prompt('', panel_data.$titleText[0].textContent,
// We will execute this function when user clicks on the OK button
function (evt, value) {
if (value) {
// Remove the leading and trailing white spaces.
value = value.trim();
var name = getAppropriateLabel(treeInfo);
setDebuggerTitle(panel, self.preferences, name, treeInfo.schema.label, treeInfo.database.label, value, pgAdmin.Browser);
}
},
// We will execute this function when user clicks on the Cancel
// button. Do nothing just close it.
function (evt) { evt.cancel = false; }
).set({ 'title': gettext('Rename Panel') });
panelRenameEvent(panel_data, panel, treeInfo);
});
}
@ -861,6 +846,17 @@ export default function DebuggerArgumentComponent({ debuggerInfo, restartDebug,
}
function panelRenameEvent(panel_data, panel, treeInfo) {
let name = getAppropriateLabel(treeInfo);
let preferences = pgAdmin.Browser.get_preferences_for_module('browser');
let data = {
function_name: name,
schema_name: treeInfo.schema.label,
database_name: treeInfo.database.label
};
showRenamePanel(panel_data.$titleText[0].textContent, preferences, panel, 'debugger', data);
}
return (
<Box className={classes.root}>
<Box className={classes.body}>

View File

@ -129,18 +129,7 @@ def panel(trans_id, editor_title):
trans_id=trans_id,
requirejs=True,
basejs=True,
editor_title=editor_title
)
@blueprint.route("/schema_diff.js")
@login_required
def script():
"""render the required javascript"""
return Response(
response=render_template("schema_diff/js/schema_diff.js", _=gettext),
status=200,
mimetype=MIMETYPE_APP_JS
editor_title=editor_title,
)

View File

@ -1,155 +0,0 @@
.icon-compare:before {
font-size: 1.3em !important;
}
.icon-script {
display: inline-block;
align-content: center;
vertical-align: middle;
height: 18px;
width: 18px;
background-size: 20px !important;
background-repeat: no-repeat;
background-position-x: center;
background-position-y: center;
background-image: url('../img/script.svg') !important;
}
.really-hidden {
display: none !important;
}
#schema-diff-header {
padding: 0.75rem 0.7rem;
}
#schema-diff-header .control-label {
width: 120px !important;
padding: 5px 5px !important;
}
#schema-diff-grid .slick-header-column.ui-state-default {
height: 32px !important;
line-height: 25px !important;
}
#schema-diff-grid .grid-header label {
display: inline-block;
font-weight: bold;
margin: auto auto auto 6px;
}
.grid-header .ui-icon {
margin: 4px 4px auto 6px;
background-color: transparent;
border-color: transparent;
}
.slick-row .cell-actions {
text-align: left;
}
/* Slick.Editors.Text, Slick.Editors.Date */
#schema-diff-grid .slick-header > input.editor-text {
width: 100%;
height: 100%;
border: 0;
margin: 0;
background: transparent;
outline: 0;
padding: 0;
}
/* Slick.Editors.Checkbox */
#schema-diff-grid .slick-header > input.editor-checkbox {
margin: 0;
height: 100%;
padding: 0;
border: 0;
}
.slick-row.selected .cell-selection {
background-color: transparent; /* show default selected row background */
}
#schema-diff-grid .slick-header .ui-state-default,
#schema-diff-grid .slick-header .ui-widget-content.ui-state-default,
#schema-diff-grid .slick-header .ui-widget-header .ui-state-default {
background: none;
}
#schema-diff-grid .slick-header .slick-header-column {
font-weight: bold;
display: block;
}
.slick-group-toggle.collapsed, .slick-group-toggle.expanded {
background: none !important;
width: 20px;
}
.slick-group-toggle {
margin-right: 0px !important;
height: 11px !important;
}
#schema-diff-ddl-comp .badge .caret {
display: inline-block;
margin-left: 2px;
margin-right: 4px;
width: 0.7rem;
}
#schema-diff-ddl-comp .badge {
font-size: inherit;
padding: 7px;
}
#schema-diff-ddl-comp .accordian-group {
padding: 0px;
}
#ddl_comp_fetching_data.pg-sp-container {
height: 100%;
bottom: 10px;
.pg-sp-content {
position: absolute;
width: 100%;
}
}
.ddl-copy {
z-index: 10;
position: absolute;
right: 1px;
top: 1px;
}
#schema-diff-grid .pg-panel-message {
font-size: 0.875rem;
}
#schema-diff-ddl-comp .sql_field_layout {
overflow: auto !important;
height: 100%;
}
#schema-diff-ddl-comp .source_ddl, #schema-diff-ddl-comp .target_ddl, #schema-diff-ddl-comp .diff_ddl {
height: 300px;
overflow: hidden;
}
.target-buttons {
flex-wrap: wrap;
max-width: 40% !important;
}
.slick-cell .ml-2 {
margin-left: 2rem !important;
}
.slick-cell .ml-3 {
margin-left: 3rem !important;
}

View File

@ -0,0 +1,65 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
export const PANELS = {
SCHEMADIFF: 'id-schema-diff',
RESULTS: 'id-results',
};
export const TYPE = {
SOURCE: 1,
TARGET: 2
};
export const MENUS = {
COMPARE: 'schema-diff-compare',
GENERATE_SCRIPT: 'schema-diff-generate-script',
FILTER: 'schema-diff-filter'
};
export const STYLE_CONSTANT = {
IDENTICAL: 'identical',
DIFFERENT :'different',
SOURCE_ONLY: 'source',
TARGET_ONLY: 'target'
};
export const MENUS_COMPARE_CONSTANT = {
COMPARE_IGNORE_OWNER: 1,
COMPARE_IGNORE_WHITESPACE: 2
};
export const MENUS_FILTER_CONSTANT = {
FILTER_IDENTICAL: 1,
FILTER_DIFFERENT: 2,
FILTER_SOURCE_ONLY: 3,
FILTER_TARGET_ONLY: 4,
};
export const SCHEMA_DIFF_EVENT = {
TRIGGER_SELECT_SERVER: 'TRIGGER_SELECT_SERVER',
TRIGGER_SELECT_DATABASE: 'TRIGGER_SELECT_DATABASE',
TRIGGER_SELECT_SCHEMA: 'TRIGGER_SELECT_DATABASE',
TRIGGER_COMPARE_DIFF: 'TRIGGER_COMPARE_DIFF',
TRIGGER_GENERATE_SCRIPT: 'TRIGGER_GENERATE_SCRIPT',
TRIGGER_CHANGE_FILTER: 'TRIGGER_CHANGE_FILTER',
TRIGGER_CHANGE_RESULT_SQL: 'TRIGGER_CHANGE_RESULT_SQL',
TRIGGER_ROW_SELECT: 'TRIGGER_ROW_SELECT',
};
export const FILTER_NAME = {
IDENTICAL : gettext('Identical'),
DIFFERENT : gettext('Different'),
SOURCE_ONLY: gettext('Source Only'),
TARGET_ONLY: gettext('Target Only')
};

View File

@ -0,0 +1,165 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import ReactDOM from 'react-dom';
import gettext from 'sources/gettext';
import url_for from 'sources/url_for';
import pgWindow from 'sources/window';
import { registerDetachEvent } from 'sources/utils';
import { _set_dynamic_tab } from '../../../sqleditor/static/js/show_query_tool';
import getApiInstance from '../../../../static/js/api_instance';
import Theme from '../../../../static/js/Theme';
import ModalProvider from '../../../../static/js/helpers/ModalProvider';
import Notify from '../../../../static/js/helpers/Notifier';
import SchemaDiffComponent from './components/SchemaDiffComponent';
import { showRenamePanel } from '../../../../static/js/Dialogs';
export default class SchemaDiff {
static instance;
static getInstance(...args) {
if (!SchemaDiff.instance) {
SchemaDiff.instance = new SchemaDiff(...args);
}
return SchemaDiff.instance;
}
constructor(pgAdmin, pgBrowser) {
this.pgAdmin = pgAdmin;
this.pgBrowser = pgBrowser;
this.wcDocker = window.wcDocker;
this.api = getApiInstance();
}
init() {
let self = this;
if (self.initialized)
return;
self.initialized = true;
// Define the nodes on which the menus to be appear
self.pgBrowser.add_menus([{
name: 'schema_diff',
module: self,
applies: ['tools'],
callback: 'showSchemaDiffTool',
priority: 1,
label: gettext('Schema Diff'),
enable: true,
below: true,
}]);
/* Create and load the new frame required for schema diff panel */
self.frame = new self.pgBrowser.Frame({
name: 'frm_schemadiff',
title: gettext('Schema Diff'),
showTitle: true,
isCloseable: true,
isRenamable: true,
isPrivate: true,
icon: 'pg-font-icon icon-compare',
url: 'about:blank',
});
/* Cache may take time to load for the first time. Keep trying till available */
let cacheIntervalId = setInterval(function () {
if (self.pgBrowser.preference_version() > 0) {
self.preferences = self.pgBrowser.get_preferences_for_module('schema_diff');
clearInterval(cacheIntervalId);
}
}, 0);
self.pgBrowser.onPreferencesChange('schema_diff', function () {
self.preferences = self.pgBrowser.get_preferences_for_module('schema_diff');
});
self.frame.load(self.pgBrowser.docker);
}
showSchemaDiffTool() {
let self = this;
self.api({
url: url_for('schema_diff.initialize', null),
method: 'GET',
})
.then(function (res) {
self.trans_id = res.data.data.schemaDiffTransId;
res.data.data.panel_title = gettext('Schema Diff');
self.launchSchemaDiff(res.data.data);
})
.catch(function (error) {
Notify.error(gettext(`Error in schema diff initialize ${error.response.data}`));
});
}
launchSchemaDiff(data) {
let self = this;
let panelTitle = data.panel_title,
trans_id = data.schemaDiffTransId,
panelTooltip = '';
let url_params = {
'trans_id': trans_id,
'editor_title': panelTitle,
},
baseUrl = url_for('schema_diff.panel', url_params);
let browserPreferences = this.pgBrowser.get_preferences_for_module('browser');
let openInNewTab = browserPreferences.new_browser_tab_open;
if (openInNewTab && openInNewTab.includes('schema_diff')) {
window.open(baseUrl, '_blank');
// Send the signal to runtime, so that proper zoom level will be set.
setTimeout(function () {
this.pgBrowser.send_signal_to_runtime('Runtime new window opened');
}, 500);
} else {
this.pgBrowser.Events.once(
'pgadmin-browser:frame:urlloaded:frm_schemadiff',
function (frame) {
frame.openURL(baseUrl);
});
let propertiesPanel = this.pgBrowser.docker.findPanels('properties'),
schemaDiffPanel = this.pgBrowser.docker.addPanel('frm_schemadiff', this.wcDocker.DOCK.STACKED, propertiesPanel[0]);
registerDetachEvent(schemaDiffPanel);
// Panel Rename event
schemaDiffPanel.on(self.wcDocker.EVENT.RENAME, function (panel_data) {
self.panel_rename_event(panel_data, schemaDiffPanel, browserPreferences);
});
_set_dynamic_tab(this.pgBrowser, browserPreferences['dynamic_tabs']);
// Set panel title and icon
schemaDiffPanel.title('<span title="' + panelTooltip + '">' + panelTitle + '</span>');
schemaDiffPanel.icon('pg-font-icon icon-compare');
schemaDiffPanel.focus();
}
}
panel_rename_event(panel_data, panel) {
showRenamePanel(panel_data.$titleText[0].textContent, null, panel);
}
load(container, trans_id) {
ReactDOM.render(
<Theme>
<ModalProvider>
<SchemaDiffComponent params={{ transId: trans_id, pgAdmin: pgWindow.pgAdmin }}></SchemaDiffComponent>
</ModalProvider>
</Theme>,
container
);
}
}

View File

@ -0,0 +1,143 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { useContext, useState } from 'react';
import { Box, Grid, Typography } from '@material-ui/core';
import { makeStyles } from '@material-ui/styles';
import { InputSelect } from '../../../../../static/js/components/FormComponents';
import { SchemaDiffEventsContext } from './SchemaDiffComponent';
import { SCHEMA_DIFF_EVENT } from '../SchemaDiffConstants';
const useStyles = makeStyles(() => ({
root: {
padding: '0rem'
},
spanLabel: {
alignSelf: 'center',
marginRight: '4px',
},
inputLabel: {
padding: '0.3rem',
},
}));
export function InputComponent({ label, serverList, databaseList, schemaList, diff_type, selectedSid = null, selectedDid=null, selectedScid=null }) {
const classes = useStyles();
const [selectedServer, setSelectedServer] = useState(selectedSid);
const [selectedDatabase, setSelectedDatabase] = useState(selectedDid);
const [selectedSchema, setSelectedSchema] = useState(selectedScid);
const eventBus = useContext(SchemaDiffEventsContext);
const [disableDBSelection, setDisableDBSelection] = useState(selectedSid == null ? true : false);
const [disableSchemaSelection, setDisableSchemaSelection] = useState(selectedDid == null ? true : false);
const changeServer = (selectedOption) => {
setDisableDBSelection(false);
setSelectedServer(selectedOption);
// Reset the Database selection if user deselect server from DD
if(selectedOption == null){
setSelectedDatabase(null);
setDisableDBSelection(true);
setSelectedSchema(null);
setDisableSchemaSelection(true);
}
eventBus.fireEvent(SCHEMA_DIFF_EVENT.TRIGGER_SELECT_SERVER, { selectedOption, diff_type, serverList });
};
const changeDatabase = (selectedDB) => {
setSelectedDatabase(selectedDB);
setDisableSchemaSelection(false);
// Reset the Schema selection if user deselect database from DD
if(selectedDB == null){
setSelectedSchema(null);
setDisableSchemaSelection(true);
}
eventBus.fireEvent(SCHEMA_DIFF_EVENT.TRIGGER_SELECT_DATABASE, {selectedServer, selectedDB, diff_type, databaseList});
};
const changeSchema = (selectedSC) => {
setSelectedSchema(selectedSC);
eventBus.fireEvent(SCHEMA_DIFF_EVENT.TRIGGER_SELECT_SCHEMA, { selectedSC, diff_type });
};
return (
<Box className={classes.root}>
<Grid
container
direction="row"
alignItems="center"
key={_.uniqueId('c')}
>
<Grid item lg={2} md={2} sm={2} xs={2} className={classes.inputLabel} key={_.uniqueId('c')}>
<Typography id={label}>{label}</Typography>
</Grid>
<Grid item lg={4} md={4} sm={4} xs={4} className={classes.inputLabel} key={_.uniqueId('c')}>
<InputSelect
options={serverList}
onChange={changeServer}
value={selectedServer}
controlProps={
{
placeholder: 'Select server...'
}
}
key={'server_' + diff_type}
></InputSelect>
</Grid>
<Grid item lg={3} md={3} sm={3} xs={3} className={classes.inputLabel} key={_.uniqueId('c')}>
<InputSelect
options={databaseList}
onChange={changeDatabase}
value={selectedDatabase}
controlProps={
{
placeholder: 'Select Database...'
}
}
key={'database_' + diff_type}
readonly={disableDBSelection}
></InputSelect>
</Grid>
<Grid item lg={3} md={3} sm={3} xs={3} className={classes.inputLabel} key={_.uniqueId('c')}>
<InputSelect
options={schemaList}
onChange={changeSchema}
value={selectedSchema}
controlProps={
{
placeholder: 'Select Schema...'
}
}
key={'schema' + diff_type}
readonly={disableSchemaSelection}
></InputSelect>
</Grid>
</Grid>
</Box >
);
}
InputComponent.propTypes = {
label: PropTypes.string,
serverList: PropTypes.array,
databaseList:PropTypes.array,
schemaList:PropTypes.array,
diff_type:PropTypes.number,
selectedSid: PropTypes.number,
selectedDid: PropTypes.number,
selectedScid:PropTypes.number,
};

View File

@ -0,0 +1,739 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import _ from 'lodash';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import { SelectColumn } from 'react-data-grid';
import React, { useContext, useEffect, useLayoutEffect, useReducer, useRef, useState } from 'react';
import { Box } from '@material-ui/core';
import { makeStyles } from '@material-ui/styles';
import KeyboardArrowRightRoundedIcon from '@material-ui/icons/KeyboardArrowRightRounded';
import KeyboardArrowDownRoundedIcon from '@material-ui/icons/KeyboardArrowDownRounded';
import InfoIcon from '@material-ui/icons/InfoRounded';
import gettext from 'sources/gettext';
import url_for from 'sources/url_for';
import { FILTER_NAME, SCHEMA_DIFF_EVENT } from '../SchemaDiffConstants';
import { SchemaDiffContext, SchemaDiffEventsContext } from './SchemaDiffComponent';
import { InputCheckbox } from '../../../../../static/js/components/FormComponents';
import PgReactDataGrid from '../../../../../static/js/components/PgReactDataGrid';
import Notifier from '../../../../../static/js/helpers/Notifier';
const useStyles = makeStyles((theme) => ({
root: {
paddingTop: '0.5rem',
display: 'flex',
height: '100%',
flexDirection: 'column',
color: theme.palette.text.primary,
backgroundColor: theme.otherVars.qtDatagridBg,
border: 'none',
fontSize: '13px',
'& .rdg': {
flex: 1,
borderTop: '1px solid' + theme.otherVars.borderColor,
},
'--rdg-background-color': theme.palette.default.main,
'--rdg-selection-color': theme.palette.primary.main,
'& .rdg-cell': {
padding: 0,
boxShadow: 'none',
color: theme.otherVars.schemaDiff.diffColorFg + ' !important',
...theme.mixins.panelBorder.right,
...theme.mixins.panelBorder.bottom,
'&[aria-colindex="1"]': {
padding: 0,
},
'&[aria-selected=true]:not([role="columnheader"]):not([aria-colindex="1"])': {
outlineWidth: '0',
outlineOffset: '-1px',
color: theme.otherVars.qtDatagridSelectFg,
},
'&[aria-selected=true][aria-colindex="1"]': {
outlineWidth: 0,
}
},
'& .rdg-header-row .rdg-cell': {
padding: 0,
paddingLeft: '0.5rem',
boxShadow: 'none',
},
'& .rdg-header-row': {
backgroundColor: theme.palette.background.default,
},
'& .rdg-row': {
backgroundColor: theme.palette.background.default,
'&[aria-selected=true]': {
backgroundColor: theme.palette.primary.light,
color: theme.otherVars.qtDatagridSelectFg,
'& .rdg-cell:nth-child(1)': {
backgroundColor: 'transparent',
outlineColor: 'transparent',
color: theme.palette.primary.contrastText,
}
},
}
},
grid: {
fontSize: '13px',
'--rdg-selection-color': 'none'
},
subRow: {
paddingLeft: '1rem'
},
recordRow: {
marginLeft: '2.7rem',
height: '1.3rem',
width: '1.3rem',
display: 'inline-block',
marginRight: '0.3rem',
paddingLeft: '0.5rem',
},
rowIcon: {
display: 'inline-block !important',
height: '1.3rem',
width: '1.3rem'
},
cellExpand: {
float: 'inline-end',
display: 'table',
blockSize: '100%',
'& span': {
display: 'table-cell',
verticalAlign: 'middle',
cursor: 'pointer',
}
},
gridPanel: {
'--rdg-background-color': theme.palette.default.main + ' !important',
},
source: {
backgroundColor: theme.otherVars.schemaDiff.sourceRowColor,
color: theme.otherVars.schemaDiff.diffSelectFG,
paddingLeft: '0.5rem',
},
target: {
backgroundColor: theme.otherVars.schemaDiff.targetRowColor,
color: theme.otherVars.schemaDiff.diffSelectFG,
paddingLeft: '0.5rem',
},
different: {
backgroundColor: theme.otherVars.schemaDiff.diffRowColor,
color: theme.otherVars.schemaDiff.diffSelectFG,
paddingLeft: '0.5rem',
},
identical: {
paddingLeft: '0.5rem',
color: theme.otherVars.schemaDiff.diffSelectFG,
},
selectCell: {
padding: '0 0.3rem'
},
headerSelectCell: {
padding: '0rem 0.3rem 0 0.3rem'
},
count: {
display: 'inline-block !important',
},
countStyle: {
fontWeight: 900,
fontSize: '0.8rem',
paddingLeft: '0.3rem',
},
countLabel: {
paddingLeft: '1rem',
},
selectedRow: {
paddingLeft: '0.5rem',
backgroundColor: theme.palette.primary.light
},
selectedRowCheckBox: {
paddingLeft: '0.5rem',
backgroundColor: theme.palette.primary.light,
},
selChBox: {
paddingLeft: 0,
},
noRowsIcon:{
width: '1.1rem',
height: '1.1rem',
marginRight: '0.5rem',
}
}));
function useFocusRef(isSelected) {
const ref = useRef(null);
useLayoutEffect(() => {
if (!isSelected) return;
ref.current?.focus({ preventScroll: true });
}, [isSelected]);
return {
ref,
tabIndex: isSelected ? 0 : -1
};
}
function setRecordCount(row, filterParams) {
row['identicalCount'] = 0;
row['differentCount'] = 0;
row['sourceOnlyCount'] = 0;
row['targetOnlyCount'] = 0;
row.children.map((ch) => {
if (filterParams.includes(ch.status)) {
if (ch.status == FILTER_NAME.IDENTICAL) {
row['identicalCount'] = row['identicalCount'] + 1;
} else if (ch.status == FILTER_NAME.DIFFERENT) {
row['differentCount'] = row['differentCount'] + 1;
} else if (ch.status == FILTER_NAME.SOURCE_ONLY) {
row['sourceOnlyCount'] = row['sourceOnlyCount'] + 1;
} else if (ch.status == FILTER_NAME.TARGET_ONLY) {
row['targetOnlyCount'] = row['targetOnlyCount'] + 1;
}
}
});
}
function CellExpanderFormatter({
row,
isCellSelected,
expanded,
filterParams,
onCellExpand
}) {
const classes = useStyles();
const { ref, tabIndex } = useFocusRef(isCellSelected);
'identicalCount' in row && setRecordCount(row, filterParams);
function handleKeyDown(e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
onCellExpand();
}
}
return (
<div className={classes.cellExpand}>
<span onClick={onCellExpand} onKeyDown={handleKeyDown}>
<span ref={ref} tabIndex={tabIndex} className={'identicalCount' in row ? classes.subRow : null}>
{expanded ? <KeyboardArrowDownRoundedIcon /> : <KeyboardArrowRightRoundedIcon />} <span className={clsx(row.icon, classes.rowIcon)}></span>{row.label}
{
'identicalCount' in row ?
<span className={clsx(classes.count)}>
<span className={classes.countLabel}>{FILTER_NAME.IDENTICAL}:</span> <span className={classes.countStyle}>{row.identicalCount} </span>
<span className={classes.countLabel}>{FILTER_NAME.DIFFERENT}:</span> <span className={classes.countStyle}>{row.differentCount} </span>
<span className={classes.countLabel}>{FILTER_NAME.SOURCE_ONLY}:</span> <span className={classes.countStyle}>{row.sourceOnlyCount} </span>
<span className={classes.countLabel}>{FILTER_NAME.TARGET_ONLY}: </span><span className={classes.countStyle}>{row.targetOnlyCount}</span>
</span>
: null
}
</span>
</span>
</div>
);
}
CellExpanderFormatter.propTypes = {
row: PropTypes.object,
isCellSelected: PropTypes.bool,
expanded: PropTypes.bool,
onCellExpand: PropTypes.func,
filterParams: PropTypes.array,
};
function toggleSubRow(rows, id, filterParams) {
const newRows = [...rows];
const rowIndex = newRows.findIndex((r) => r.id === id);
const row = newRows[rowIndex];
if (!row) return newRows;
const { children } = row;
if (!children) return newRows;
if (children.length > 0) {
newRows[rowIndex] = { ...row, isExpanded: !row.isExpanded };
if (!row.isExpanded) {
let tempChild = [];
expandRows(children, filterParams, tempChild, newRows, rowIndex);
} else {
collapseRows(newRows, filterParams, rowIndex);
}
return newRows;
} else {
newRows.splice(rowIndex, 1);
return newRows;
}
}
function expandRows(children, filterParams, tempChild, newRows, rowIndex) {
children.map((child) => {
if ('children' in child) {
let tempSubChild = [];
child.children.map((subChild) => {
if (filterParams.includes(subChild.status)) {
tempSubChild.push(subChild);
}
});
if (tempSubChild.length > 0) {
tempChild.push(child);
}
}
else {
if (filterParams.includes(child.status)) {
tempChild.push(child);
}
}
});
if (tempChild.length > 0) {
newRows.splice(rowIndex + 1, 0, ...tempChild);
} else {
newRows.splice(rowIndex, 1);
}
}
function collapseRows(newRows, filterParams, rowIndex) {
let totalChild = 0;
let filteredChild = newRows[rowIndex].children.filter((el) => {
if (el?.children) {
let clist = el.children.filter((subch) => {
return filterParams.includes(subch.status);
});
if (clist.length > 0) {
return el;
}
} else {
return el?.status ? filterParams.includes(el.status) : el;
}
});
let totalChCount = filteredChild.length;
for (let i = 0; i <= filteredChild.length; i++) {
let _index = i + 1;
let indx = totalChild ? rowIndex + totalChild + _index : rowIndex + _index;
if (newRows[indx]?.isExpanded) {
let filteredSubChild = newRows[indx].children.filter((el) => {
return filterParams.includes(el.status);
});
totalChild += filteredSubChild.length;
}
}
newRows.splice(rowIndex + 1, totalChild ? totalChCount + totalChild : totalChCount);
}
function getChildrenRows(data) {
if ('children' in data) {
return data.children;
}
return data;
}
function checkRowExpanedStatus(rows, record) {
if (rows.length > 0) {
let tempRecord = rows.filter((rec) => {
return rec.parentId == record.parentId && rec.label == record.label;
});
return tempRecord.length > 0 ? tempRecord[0].isExpanded : false;
}
return false;
}
function prepareRows(rows, gridData, filterParams) {
let newRows = [];
let adedIds = [];
gridData.map((record) => {
let childrens = getChildrenRows(record);
if (childrens.length > 0) {
childrens.map((child) => {
let subChildrens = getChildrenRows(child);
let tempChildList = [];
subChildrens.map((subChild) => {
if (filterParams.includes(subChild.status)) {
tempChildList.push(subChild);
adedIds.push(subChild.id);
}
});
if (!adedIds.includes(record.id) && tempChildList.length > 0) {
adedIds.push(record.id);
record.isExpanded = true;
newRows.push(record);
}
if (!adedIds.includes(child.id) && tempChildList.length > 0) {
adedIds.push(child.id);
child.isExpanded = checkRowExpanedStatus(rows, child);
newRows.push(child);
newRows = checkAndAddChild(child, newRows, tempChildList);
}
});
}
});
return newRows;
}
function checkAndAddChild(child, newRows, tempChildList) {
if (child.isExpanded) {
newRows = newRows.concat(tempChildList);
}
return newRows;
}
function reducer(rows, { type, id, filterParams, gridData }) {
switch (type) {
case 'toggleSubRow':
return toggleSubRow(rows, id, filterParams);
case 'applyFilter':
return prepareRows(rows, gridData, filterParams);
default:
return rows;
}
}
function getStyleClassName(row, selectedRowIds, isCellSelected, activeRowId, isCheckbox = false) {
const classes = useStyles();
let clsName = null;
if (selectedRowIds.includes(`${row.id}`) || isCellSelected || row.id == activeRowId) {
clsName = isCheckbox ? classes.selectedRowCheckBox : classes.selectedRow;
} else {
if (row.status == FILTER_NAME.DIFFERENT) {
clsName = classes.different;
} else if (row.status == FILTER_NAME.SOURCE_ONLY) {
clsName = classes.source;
} else if (row.status == FILTER_NAME.TARGET_ONLY) {
clsName = classes.target;
} else if (row.status == FILTER_NAME.IDENTICAL) {
clsName = classes.identical;
}
}
return clsName;
}
export function ResultGridComponent({ gridData, allRowIds, filterParams, selectedRowIds, transId, sourceData, targetData }) {
const classes = useStyles();
const [rows, dispatch] = useReducer(reducer, [...gridData]);
const [selectedRows, setSelectedRows] = useState([]);
const [rootSelection, setRootSelection] = useState(false);
const [activeRow, setActiveRow] = useState(null);
const schemaDiffToolContext = useContext(SchemaDiffContext);
function checkAllChildInclude(row, tempSelectedRows) {
let isChildAllInclude = true;
row.metadata.children.map((id) => {
if (!tempSelectedRows.includes(id) && id !== `${row.id}`) {
isChildAllInclude = false;
}
});
return isChildAllInclude;
}
function selectedResultRows(row, tempSelectedRows) {
if (row.metadata.isRoot) {
tempSelectedRows.push(`${row.id}`, ...row.metadata.children);
tempSelectedRows.push(...row.metadata.subChildren);
} else if (row.metadata.subChildren) {
tempSelectedRows.push(...row.metadata.dependencies);
tempSelectedRows.push(`${row.id}`, ...row.metadata.subChildren);
let isChildAllInclude = checkAllChildInclude(row, tempSelectedRows);
isChildAllInclude && tempSelectedRows.push(`${row.metadata.parentId}`);
} else {
tempSelectedRows.push(...row.dependencieRowIds);
tempSelectedRows.push(`${row.id}`);
let isChildAllInclude = checkAllChildInclude(row, tempSelectedRows);
isChildAllInclude && tempSelectedRows.push(`${row.metadata.parentId}`);
for (let i = 0; i < rows.length; i++) {
if (rows[i].id == row.metadata.parentId) {
let isChildInclude = checkAllChildInclude(rows[i], tempSelectedRows);
isChildInclude && tempSelectedRows.push(`${rows[i].metadata.parentId}`);
break;
}
}
}
}
function deselectChildAndSubChild(children, tempSelectedRows) {
children.map((chid) => {
let indx = tempSelectedRows.indexOf(chid);
indx != -1 && tempSelectedRows.splice(indx, 1);
});
}
function deselectResultRows(row, tempSelectedRows) {
if (row.metadata.isRoot) {
deselectChildAndSubChild(row.metadata.subChildren, tempSelectedRows);
deselectChildAndSubChild(row.metadata.children, tempSelectedRows);
let rootIndex = tempSelectedRows.indexOf(`${row.id}`);
rootIndex != -1 && tempSelectedRows.splice(rootIndex, 1);
} else if (row.metadata.subChildren) {
deselectChildAndSubChild(row.metadata.subChildren, tempSelectedRows);
let isChildAllInclude = true;
row.metadata.children.map((id) => {
if (tempSelectedRows.includes(id)) {
isChildAllInclude = false;
}
});
let rootIndex = tempSelectedRows.indexOf(`${row.id}`);
rootIndex != -1 && tempSelectedRows.splice(rootIndex, 1);
let parentIndex = tempSelectedRows.indexOf(`${row.metadata.parentId}`);
(!isChildAllInclude && parentIndex != -1) && tempSelectedRows.splice(parentIndex, 1);
row.metadata.dependencies.map((depid) => {
let depIndex = tempSelectedRows.indexOf(`${depid}`);
depIndex != -1 && tempSelectedRows.splice(depIndex, 1);
});
} else {
let elementIndex = tempSelectedRows.indexOf(`${row.id}`);
elementIndex != -1 && tempSelectedRows.splice(elementIndex, 1);
let parentElIndex = tempSelectedRows.indexOf(`${row.metadata.parentId}`);
parentElIndex != -1 && tempSelectedRows.splice(parentElIndex, 1);
let rootIndex = tempSelectedRows.indexOf(`${row.metadata.rootId}`);
rootIndex != -1 && tempSelectedRows.splice(rootIndex, 1);
row.dependencieRowIds.map((id) => {
let deptRowIndex = tempSelectedRows.indexOf(`${id}`);
deptRowIndex != -1 && tempSelectedRows.splice(deptRowIndex, 1);
});
}
}
const columns = [
{
key: 'id',
...SelectColumn,
minWidth: 30,
width: 30,
headerRenderer() {
return (
<InputCheckbox
cid={_.uniqueId('rgc')}
className={classes.headerSelectCell}
value={selectedRows.length == allRowIds.length ? rootSelection : false}
size='small'
onChange={(e) => {
if (e.target.checked) {
setRootSelection(true);
setSelectedRows([...allRowIds]);
selectedRowIds([...allRowIds]);
} else {
setRootSelection(false);
setSelectedRows([]);
selectedRowIds([]);
}
}
}
></InputCheckbox>
);
},
formatter({ row, isCellSelected }) {
isCellSelected && setActiveRow(row.id);
return (
<Box className={!row?.children && clsx(getStyleClassName(row, selectedRows, isCellSelected, activeRow, true), classes.selChBox)}>
<InputCheckbox
className={classes.selectCell}
cid={`${row.id}`}
value={selectedRows.includes(`${row.id}`)}
size='small'
onChange={(e) => {
setSelectedRows((prev) => {
let tempSelectedRows = [...prev];
if (!prev.includes(e.target.id)) {
selectedResultRows(row, tempSelectedRows);
tempSelectedRows.length === allRowIds.length && setRootSelection(true);
} else {
deselectResultRows(row, tempSelectedRows);
}
tempSelectedRows = new Set(tempSelectedRows);
selectedRowIds([...tempSelectedRows]);
return [...tempSelectedRows];
});
}
}
></InputCheckbox>
</Box>
);
}
},
{
key: 'label',
name: 'Objects',
width: '80%',
colSpan(args) {
if (args.type === 'ROW' && 'children' in args.row) {
return 2;
}
return 1;
},
formatter({ row, isCellSelected }) {
const hasChildren = row.children !== undefined;
isCellSelected && setActiveRow(row.id);
return (
<>
{hasChildren && (
<CellExpanderFormatter
row={row}
isCellSelected={isCellSelected}
expanded={row.isExpanded === true}
filterParams={filterParams}
onCellExpand={() => dispatch({ id: row.id, type: 'toggleSubRow', filterParams: filterParams, gridData: gridData, selectedRows: selectedRows })}
/>
)}
<div className="rdg-cell-value">
{!hasChildren && (
<Box className={clsx(getStyleClassName(row, selectedRows, isCellSelected, activeRow), classes.status)}>
<span className={clsx(classes.recordRow, row.icon)}></span>
{row.label}
</Box>
)}
</div>
</>
);
}
},
{
key: 'status',
name: 'Comparison Result',
formatter({ row, isCellSelected }) {
isCellSelected && setActiveRow(row.id);
return (
<Box className={getStyleClassName(row, selectedRows, isCellSelected, activeRow)}>
{row.status}
</Box>
);
}
},
];
useEffect(() => {
let tempRows = gridData;
tempRows.map((row) => {
dispatch({ id: row.id, type: 'applyFilter', filterParams: filterParams, gridData: gridData, selectedRows: selectedRows });
});
}, [filterParams]);
const eventBus = useContext(SchemaDiffEventsContext);
const rowSelection = (row) => {
if (row.ddlData != undefined && row.status != FILTER_NAME.IDENTICAL) {
eventBus.fireEvent(SCHEMA_DIFF_EVENT.TRIGGER_CHANGE_RESULT_SQL, row.ddlData);
} else if (row.status == FILTER_NAME.IDENTICAL) {
let url_params = {
'trans_id': transId,
'source_sid': sourceData.sid,
'source_did': sourceData.did,
'source_scid': row.source_scid,
'target_sid': targetData.sid,
'target_did': targetData.did,
'target_scid': row.target_scid,
'comp_status': row.status,
'source_oid': row.source_oid,
'target_oid': row.target_oid,
'node_type': row.itemType,
};
let baseUrl = url_for('schema_diff.ddl_compare', url_params);
schemaDiffToolContext.api.get(baseUrl).then((res) => {
row.ddlData = {
'SQLdiff': res.data.diff_ddl,
'sourceSQL': res.data.source_ddl,
'targetSQL': res.data.target_ddl
};
eventBus.fireEvent(SCHEMA_DIFF_EVENT.TRIGGER_CHANGE_RESULT_SQL, row.ddlData);
}).catch((err) => {
Notifier.alert(err.message);
});
}
};
function rowKeyGetter(row) {
return row.id;
}
return (
<Box className={classes.root} flexGrow="1" minHeight="0" id="schema-diff-grid">
{
gridData ?
<PgReactDataGrid
id="schema-diff-result-grid"
columns={columns} rows={rows}
className={clsx('big-grid', classes.gridPanel, classes.grid)}
treeDepth={2}
enableRowSelect={true}
defaultColumnOptions={{
resizable: true
}}
headerRowHeight={28}
rowHeight={28}
onRowClick={rowSelection}
enableCellSelect={false}
rowKeyGetter={rowKeyGetter}
direction={'vertical-lr'}
noRowsText={gettext('No difference found')}
noRowsIcon={<InfoIcon className={classes.noRowsIcon} />}
/>
:
<>
{gettext('Loading result grid...')}
</>
}
</Box>
);
}
ResultGridComponent.propTypes = {
gridData: PropTypes.array,
allRowIds: PropTypes.array,
filterParams: PropTypes.array,
selectedRowIds: PropTypes.func,
transId: PropTypes.number,
sourceData: PropTypes.object,
targetData: PropTypes.object,
'sourceData.sid': PropTypes.number,
'sourceData.did': PropTypes.number,
'targetData.sid': PropTypes.number,
'targetData.did': PropTypes.number,
};

View File

@ -0,0 +1,140 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import React, { useContext, useState, useEffect } from 'react';
import { Box } from '@material-ui/core';
import { makeStyles } from '@material-ui/styles';
import { InputSQL } from '../../../../../static/js/components/FormComponents';
import { SchemaDiffEventsContext } from './SchemaDiffComponent';
import { SCHEMA_DIFF_EVENT } from '../SchemaDiffConstants';
const useStyles = makeStyles((theme) => ({
root: {
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
},
table: {
minWidth: 650,
},
summaryContainer: {
flexGrow: 1,
minHeight: 0,
overflow: 'auto',
},
panelTitle: {
borderBottom: '1px solid ' + theme.otherVars.borderColor,
padding: '0.5rem',
},
editorStyle: {
height: '100%'
},
editor: {
height: '100%',
padding: '0.5rem 0.2rem 2rem 0.5rem',
},
editorLabel: {
padding: '0.3rem 0.6rem 0 0.6rem',
},
header: {
padding: '0.5rem',
borderBottom: '1px solid ' + theme.otherVars.borderColor,
},
sqlContainer: {
display: 'flex',
flexDirection: 'row',
padding: '0rem 0rem 0.5rem',
flexGrow: 1,
overflow: 'hidden'
},
sqldata: {
display: 'flex',
flexGrow: 1,
flexDirection: 'column',
padding: '0.2rem 0.5rem',
width: '33.33%',
},
label: {
flexGrow: 1,
}
}));
export function Results() {
const classes = useStyles();
const [sourceSQL, setSourceSQL] = useState(null);
const [targetSQL, setTargetSQL] = useState(null);
const [sqlDiff, setSqlDiff] = useState(null);
const eventBus = useContext(SchemaDiffEventsContext);
useEffect(() => {
eventBus.registerListener(
SCHEMA_DIFF_EVENT.TRIGGER_CHANGE_RESULT_SQL, triggerUpdateResult);
}, []);
const triggerUpdateResult = (resultData) => {
setSourceSQL(resultData.sourceSQL);
setTargetSQL(resultData.targetSQL);
setSqlDiff(resultData.SQLdiff);
};
return (
<>
<Box className={classes.header}>
<span>{gettext('DDL Comparision')}</span>
</Box>
<Box className={classes.sqlContainer}>
<Box className={classes.sqldata}>
<Box className={classes.label}>{gettext('Source')}</Box>
<InputSQL
onLable={true}
value={sourceSQL}
options={{
readOnly: true,
}}
readonly={true}
/>
</Box>
<Box className={classes.sqldata}>
<Box className={classes.label}>{gettext('Target')}</Box>
<InputSQL
onLable={true}
value={targetSQL}
options={{
readOnly: true,
}}
readonly={true}
/>
</Box>
<Box className={classes.sqldata}>
<Box className={classes.label}>{gettext('Difference')}</Box>
<InputSQL
onLable={true}
value={sqlDiff}
options={{
readOnly: true,
}}
readonly={true}
/>
</Box>
</Box>
</>
);
}
Results.propTypes = {
};

View File

@ -0,0 +1,223 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { useState, useRef, useContext, useEffect } from 'react';
import gettext from 'sources/gettext';
import { Box } from '@material-ui/core';
import CompareArrowsRoundedIcon from '@material-ui/icons/CompareArrowsRounded';
import FeaturedPlayListRoundedIcon from '@material-ui/icons/FeaturedPlayListRounded';
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
import { makeStyles } from '@material-ui/styles';
import { DefaultButton, PgButtonGroup, PgIconButton, PrimaryButton } from '../../../../../static/js/components/Buttons';
import { FilterIcon } from '../../../../../static/js/components/ExternalIcon';
import { PgMenu, PgMenuItem, usePgMenuGroup } from '../../../../../static/js/components/Menu';
import { FILTER_NAME, MENUS, MENUS_COMPARE_CONSTANT, SCHEMA_DIFF_EVENT } from '../SchemaDiffConstants';
import { SchemaDiffContext, SchemaDiffEventsContext } from './SchemaDiffComponent';
const useStyles = makeStyles((theme) => ({
emptyIcon: {
width: '1.5rem'
},
diff_btn: {
marginRight: '1rem'
},
noactionBtn: {
cursor: 'default',
'&:hover': {
backgroundColor: 'inherit',
cursor: 'default'
}
},
scriptBtn: {
display: 'flex',
justifyContent: 'flex-end',
paddingRight: '0.3rem',
[theme.breakpoints.down('sm')]: {
paddingTop: '0.3rem',
flexGrow: 1,
},
},
filterBtn: {
[theme.breakpoints.down('sm')]: {
paddingTop: '0.3rem',
flexGrow: 1,
}
},
compareBtn: {
display: 'flex',
flexGrow: 1,
justifyContent: 'flex-start',
paddingLeft: '1.5rem',
[theme.breakpoints.down('sm')]: {
paddingTop: '0.3rem',
},
}
}));
export function SchemaDiffButtonComponent({ sourceData, targetData, selectedRowIds, rows, compareParams, filterParams = [FILTER_NAME.DIFFERENT, FILTER_NAME.SOURCE_ONLY, FILTER_NAME.TARGET_ONLY] }) {
const classes = useStyles();
const filterRef = useRef(null);
const compareRef = useRef(null);
const eventBus = useContext(SchemaDiffEventsContext);
const schemaDiffCtx = useContext(SchemaDiffContext);
const [selectedFilters, setSelectedFilters] = useState(filterParams);
const [selectedCompare, setSelectedCompare] = useState([]);
const [isDisableCompare, setIsDisableCompare] = useState(true);
const { openMenuName, toggleMenu, onMenuClose } = usePgMenuGroup();
useEffect(() => {
let isDisableComp = true;
if (sourceData.sid != null && sourceData.did != null && targetData.sid != null && targetData.did != null) {
isDisableComp = false;
}
setIsDisableCompare(isDisableComp);
}, [sourceData, targetData]);
useEffect(() => {
let prefCompareOptions = [];
if (!_.isUndefined(compareParams)) {
compareParams.ignoreOwner && prefCompareOptions.push(MENUS_COMPARE_CONSTANT.COMPARE_IGNORE_OWNER);
compareParams.ignoreWhitespaces && prefCompareOptions.push(MENUS_COMPARE_CONSTANT.COMPARE_IGNORE_WHITESPACE);
setSelectedCompare(prefCompareOptions);
} else {
schemaDiffCtx?.preferences_schema_diff?.ignore_owner && prefCompareOptions.push(MENUS_COMPARE_CONSTANT.COMPARE_IGNORE_OWNER);
schemaDiffCtx?.preferences_schema_diff?.ignore_whitespaces && prefCompareOptions.push(MENUS_COMPARE_CONSTANT.COMPARE_IGNORE_WHITESPACE);
setSelectedCompare(prefCompareOptions);
}
}, [schemaDiffCtx.preferences_schema_diff]);
const selectFilterOption = (option) => {
let newOptions = [];
setSelectedFilters((prev) => {
let newSelectdOptions = [...prev];
let removeIndex = newSelectdOptions.indexOf(option);
if (prev.includes(option)) {
newSelectdOptions.splice(removeIndex, 1);
} else {
newSelectdOptions.push(option);
}
newOptions = [...newSelectdOptions];
return newSelectdOptions;
});
let filterParam = newOptions;
eventBus.fireEvent(SCHEMA_DIFF_EVENT.TRIGGER_CHANGE_FILTER, { filterParams: filterParam });
};
const selectCompareOption = (option) => {
setSelectedCompare((prev) => {
let newSelectdOptions = [...prev];
let removeIndex = newSelectdOptions.indexOf(option);
if (prev.includes(option)) {
newSelectdOptions.splice(removeIndex, 1);
} else {
newSelectdOptions.push(option);
}
return newSelectdOptions;
});
};
const compareDiff = () => {
let compareParam = {
'ignoreOwner': selectedCompare.includes(MENUS_COMPARE_CONSTANT.COMPARE_IGNORE_OWNER) ? 1 : 0,
'ignoreWhitespaces': selectedCompare.includes(MENUS_COMPARE_CONSTANT.COMPARE_IGNORE_WHITESPACE) ? 1 : 0,
};
let filterParam = selectedFilters;
eventBus.fireEvent(SCHEMA_DIFF_EVENT.TRIGGER_COMPARE_DIFF, { sourceData, targetData, compareParams: compareParam, filterParams: filterParam });
eventBus.fireEvent(SCHEMA_DIFF_EVENT.TRIGGER_CHANGE_RESULT_SQL, {
sourceSQL: null,
targetSQL: null,
SQLdiff: null,
});
};
const generateScript = () => {
eventBus.fireEvent(SCHEMA_DIFF_EVENT.TRIGGER_GENERATE_SCRIPT, { sid: targetData.sid, did: targetData.did, selectedIds: selectedRowIds, rows: rows });
};
return (
<>
<Box className={classes.compareBtn}>
<PgButtonGroup size="small" disabled={isDisableCompare}>
<PrimaryButton startIcon={<CompareArrowsRoundedIcon />}
onClick={compareDiff}>{gettext('Compare')}</PrimaryButton>
<PgIconButton title={gettext('Compare')} icon={<KeyboardArrowDownIcon />} color={'primary'} splitButton
name={MENUS.COMPARE} ref={compareRef} onClick={toggleMenu} ></PgIconButton>
</PgButtonGroup>
</Box>
<Box className={classes.scriptBtn}>
<PgButtonGroup size="small" disabled={selectedRowIds?.length > 0 ? false : true}>
<DefaultButton startIcon={<FeaturedPlayListRoundedIcon />} onClick={generateScript}>{gettext('Generate Script')}</DefaultButton>
</PgButtonGroup>
</Box>
<Box className={classes.filterBtn}>
<PgButtonGroup size="small" disabled={isDisableCompare} style={{ paddingRight: '0.3rem' }}>
<DefaultButton startIcon={<FilterIcon />} className={classes.noactionBtn}
>{gettext('Filter')}</DefaultButton>
<PgIconButton title={gettext('File')} icon={<KeyboardArrowDownIcon />} splitButton
name={MENUS.FILTER} ref={filterRef} onClick={toggleMenu} ></PgIconButton>
</PgButtonGroup>
</Box>
<PgMenu
anchorRef={compareRef}
open={openMenuName == MENUS.COMPARE}
onClose={onMenuClose}
label={gettext('Compare')}
>
<PgMenuItem onClick={() => { selectCompareOption(MENUS_COMPARE_CONSTANT.COMPARE_IGNORE_OWNER); }}>
{selectedCompare.includes(MENUS_COMPARE_CONSTANT.COMPARE_IGNORE_OWNER) ? <CheckRoundedIcon /> : <span className={classes.emptyIcon}></span>}{gettext('Ignore owner')}
</PgMenuItem>
<PgMenuItem onClick={() => { selectCompareOption(MENUS_COMPARE_CONSTANT.COMPARE_IGNORE_WHITESPACE); }}>
{selectedCompare.includes(MENUS_COMPARE_CONSTANT.COMPARE_IGNORE_WHITESPACE) ? <CheckRoundedIcon /> : <span className={classes.emptyIcon}></span>}{gettext('Ignore whitespace')}
</PgMenuItem>
</PgMenu>
<PgMenu
anchorRef={filterRef}
open={openMenuName == MENUS.FILTER}
onClose={onMenuClose}
label={gettext('Filter')}
>
<PgMenuItem onClick={() => { selectFilterOption(FILTER_NAME.IDENTICAL); }}>
{selectedFilters.includes(FILTER_NAME.IDENTICAL) ? <CheckRoundedIcon /> : <span className={classes.emptyIcon}></span>} {gettext(FILTER_NAME.IDENTICAL)}
</PgMenuItem>
<PgMenuItem onClick={() => { selectFilterOption(FILTER_NAME.DIFFERENT); }}>
{selectedFilters.includes(FILTER_NAME.DIFFERENT) ? <CheckRoundedIcon /> : <span className={classes.emptyIcon}></span>} {gettext(FILTER_NAME.DIFFERENT)}
</PgMenuItem>
<PgMenuItem onClick={() => { selectFilterOption(FILTER_NAME.SOURCE_ONLY); }}>
{selectedFilters.includes(FILTER_NAME.SOURCE_ONLY) ? <CheckRoundedIcon /> : <span className={classes.emptyIcon}></span>} {gettext(FILTER_NAME.SOURCE_ONLY)}
</PgMenuItem>
<PgMenuItem onClick={() => { selectFilterOption(FILTER_NAME.TARGET_ONLY); }}>
{selectedFilters.includes(FILTER_NAME.TARGET_ONLY) ? <CheckRoundedIcon /> : <span className={classes.emptyIcon}></span>} {gettext(FILTER_NAME.TARGET_ONLY)}
</PgMenuItem>
</PgMenu>
</>
);
}
SchemaDiffButtonComponent.propTypes = {
sourceData: PropTypes.object,
targetData: PropTypes.object,
selectedRowIds: PropTypes.array,
rows: PropTypes.array,
compareParams: PropTypes.object,
filterParams: PropTypes.array
};

View File

@ -0,0 +1,812 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { useContext, useEffect, useState } from 'react';
import { Box, Grid } from '@material-ui/core';
import InfoRoundedIcon from '@material-ui/icons/InfoRounded';
import HelpIcon from '@material-ui/icons/HelpRounded';
import { makeStyles } from '@material-ui/styles';
import gettext from 'sources/gettext';
import url_for from 'sources/url_for';
import Loader from 'sources/components/Loader';
import pgWindow from 'sources/window';
import { PgButtonGroup, PgIconButton } from '../../../../../static/js/components/Buttons';
import Notifier from '../../../../../static/js/helpers/Notifier';
import ConnectServerContent from '../../../../../static/js/Dialogs/ConnectServerContent';
import { generateScript } from '../../../../sqleditor/static/js/show_query_tool';
import { FILTER_NAME, SCHEMA_DIFF_EVENT, TYPE } from '../SchemaDiffConstants';
import { InputComponent } from './InputComponent';
import { SchemaDiffButtonComponent } from './SchemaDiffButtonComponent';
import { SchemaDiffContext, SchemaDiffEventsContext } from './SchemaDiffComponent';
import { ResultGridComponent } from './ResultGridComponent';
const useStyles = makeStyles(() => ({
table: {
minWidth: 650,
},
summaryContainer: {
flexGrow: 1,
minHeight: 0,
overflow: 'auto',
},
note: {
marginTop: '1.2rem',
textAlign: 'center',
},
helpBtn: {
display: 'flex',
flexDirection: 'row-reverse',
paddingRight: '0.3rem'
},
compareComp: {
flexGrow: 1,
},
diffBtn: {
display: 'flex',
justifyContent: 'flex-end'
}
}));
function generateFinalScript(script_array, scriptHeader, script_body) {
_.each(Object.keys(script_array).reverse(), function (s) {
if (script_array[s].length > 0) {
script_body += script_array[s].join('\n') + '\n\n';
}
});
return `${scriptHeader} BEGIN; \n ${script_body} END;`;
}
function checkAndGetSchemaQuery(data, script_array) {
/* Check whether the selected object belongs to source only schema
if yes then we will have to add create schema statement before creating any other object.*/
if (!_.isUndefined(data.source_schema_name) && !_.isNull(data.source_schema_name)) {
let schema_query = '\nCREATE SCHEMA IF NOT EXISTS ' + data.source_schema_name + ';\n';
if (script_array[data.dependLevel].indexOf(schema_query) == -1) {
script_array[data.dependLevel].push(schema_query);
}
}
}
function getGenerateScriptData(rows, selectedIds, script_array) {
for (let selRowVal of rows) {
if (selectedIds.includes(`${selRowVal.id}`)) {
let data = selRowVal;
if (!_.isUndefined(data.diff_ddl)) {
if (!(data.dependLevel in script_array)) script_array[data.dependLevel] = [];
checkAndGetSchemaQuery(data, script_array);
script_array[data.dependLevel].push(data.diff_ddl);
}
}
}
}
function raiseErrorOnFail(alertTitle, xhr) {
try {
if (_.isUndefined(xhr.response.data)) {
Notifier.alert(alertTitle, gettext('Unable to get the response text.'));
} else {
let err = JSON.parse(xhr.response.data);
Notifier.alert(alertTitle, err.errormsg);
}
} catch (e) {
Notifier.alert(alertTitle, gettext(e.message));
}
}
const onHelpClick=()=>{
let url = url_for('help.static', {'filename': 'schema_diff.html'});
window.open(url, 'pgadmin_help');
};
export function SchemaDiffCompare({ params }) {
const classes = useStyles();
const schemaDiffToolContext = useContext(SchemaDiffContext);
const eventBus = useContext(SchemaDiffEventsContext);
const [showResultGrid, setShowResultGrid] = useState(false);
const [selectedSourceSid, setSelectedSourceSid] = useState(null);
const [selectedTargetSid, setSelectedTargetSid] = useState(null);
const [sourceDatabaseList, setSourceDatabaseList] = useState([]);
const [targetDatabaseList, setTargetDatabaseList] = useState([]);
const [selectedSourceDid, setSelectedSourceDid] = useState(null);
const [selectedTargetDid, setSelectedTargetDid] = useState(null);
const [sourceSchemaList, setSourceSchemaList] = useState([]);
const [targetSchemaList, setTargetSchemaList] = useState([]);
const [selectedSourceScid, setSelectedSourceScid] = useState(null);
const [selectedTargetScid, setSelectedTargetScid] = useState(null);
const [sourceGroupServerList, setSourceGroupServerList] = useState([]);
const [gridData, setGridData] = useState([]);
const [allRowIdList, setAllRowIdList] = useState([]);
const [filterOptions, setFilterOptions] = useState([]);
const [compareOptions, setCompareOptions] = useState(undefined);
const [selectedRowIds, setSelectedRowIds] = useState([]);
const [loaderText, setLoaderText] = useState(null);
const [apiResult, setApiResult] = useState([]);
const [rowDep, setRowDep] = useState({});
const [isInit, setIsInit] = useState(true);
useEffect(() => {
schemaDiffToolContext.api.get(url_for('schema_diff.servers')).then((res) => {
let groupedOptions = [];
_.forIn(res.data.data, (val, _key) => {
if (val.lenght == 0) {
return;
}
groupedOptions.push({
label: _key,
options: val
});
});
setSourceGroupServerList(groupedOptions);
}).catch((err) => {
Notifier.alert(err.message);
});
}, []);
useEffect(() => {
// Register all eventes for debugger.
eventBus.registerListener(
SCHEMA_DIFF_EVENT.TRIGGER_SELECT_SERVER, triggerSelectServer);
eventBus.registerListener(
SCHEMA_DIFF_EVENT.TRIGGER_SELECT_DATABASE, triggerSelectDatabase);
eventBus.registerListener(
SCHEMA_DIFF_EVENT.TRIGGER_SELECT_SCHEMA, triggerSelectSchema);
eventBus.registerListener(
SCHEMA_DIFF_EVENT.TRIGGER_COMPARE_DIFF, triggerCompareDiff);
eventBus.registerListener(
SCHEMA_DIFF_EVENT.TRIGGER_CHANGE_FILTER, triggerChangeFilter);
eventBus.registerListener(
SCHEMA_DIFF_EVENT.TRIGGER_GENERATE_SCRIPT, triggerGenerateScript);
}, []);
function checkAndSetSourceData(diff_type, selectedOption) {
if(selectedOption == null) {
setSelectedRowIds([]);
setGridData([]);
if (diff_type == TYPE.SOURCE) {
setSelectedSourceSid(null);
setSelectedSourceDid(null);
setSelectedSourceScid(null);
} else {
setSelectedTargetSid(null);
setSelectedTargetDid(null);
setSelectedTargetScid(null);
}
}
}
function setSourceTargetSid(diff_type, selectedOption) {
if (diff_type == TYPE.SOURCE) {
setSelectedSourceSid(selectedOption);
} else {
setSelectedTargetSid(selectedOption);
}
}
const triggerSelectServer = ({ selectedOption, diff_type, serverList }) => {
checkAndSetSourceData(diff_type, selectedOption);
for (const group of serverList) {
for (const opt of group.options) {
if (opt.value == selectedOption) {
if (!opt.connected) {
connectServer(selectedOption, diff_type, null, serverList);
break;
} else {
setSourceTargetSid(diff_type, selectedOption);
getDatabaseList(selectedOption, diff_type);
}
}
}
}
setSourceGroupServerList(serverList);
};
const triggerSelectDatabase = ({ selectedServer, selectedDB, diff_type, databaseList }) => {
if(selectedDB == null) {
setGridData([]);
}
if (databaseList) {
for (const opt of databaseList) {
if (opt.value == selectedDB) {
if (!opt.connected) {
connectDatabase(selectedServer, selectedDB, diff_type, databaseList);
break;
} else {
getSchemaList(selectedServer, selectedDB, diff_type);
break;
}
}
}
if (diff_type == TYPE.SOURCE) {
setSelectedSourceDid(selectedDB);
setSourceDatabaseList(databaseList);
} else {
setSelectedTargetDid(selectedDB);
setTargetDatabaseList(databaseList);
}
}
};
const triggerSelectSchema = ({ selectedSC, diff_type }) => {
if (diff_type == TYPE.SOURCE) {
setSelectedSourceScid(selectedSC);
} else {
setSelectedTargetScid(selectedSC);
}
};
const triggerCompareDiff = ({ sourceData, targetData, compareParams, filterParams }) => {
setGridData([]);
setIsInit(false);
if (JSON.stringify(sourceData) === JSON.stringify(targetData)) {
Notifier.alert(gettext('Selection Error'),
gettext('Please select the different source and target.'));
} else {
getCompareStatus();
let schemaDiffPollInterval = setInterval(getCompareStatus, 1000);
setLoaderText('Comparing objects... (this may take a few minutes)...');
let url_params = {
'trans_id': params.transId,
'source_sid': sourceData['sid'],
'source_did': sourceData['did'],
'target_sid': targetData['sid'],
'target_did': targetData['did'],
'ignore_owner': compareParams['ignoreOwner'],
'ignore_whitespaces': compareParams['ignoreWhitespaces'],
};
let baseUrl = url_for('schema_diff.compare_database', url_params);
if (sourceData['scid'] != null && targetData['scid'] != null) {
url_params['source_scid'] = sourceData['scid'];
url_params['target_scid'] = targetData['scid'];
baseUrl = url_for('schema_diff.compare_schema', url_params);
}
setCompareOptions(compareParams);
schemaDiffToolContext.api.get(baseUrl).then((res) => {
setShowResultGrid(true);
setLoaderText(null);
clearInterval(schemaDiffPollInterval);
setFilterOptions(filterParams);
getResultGridData(res.data.data, filterParams);
}).catch((err) => {
setLoaderText(null);
setShowResultGrid(false);
Notifier.error(gettext(err.message));
});
}
};
const triggerChangeFilter = ({ filterParams }) => {
setFilterOptions(filterParams);
};
const triggerGenerateScript = ({ sid, did, selectedIds, rows }) => {
setLoaderText(gettext('Generating script...'));
let generatedScript = undefined, scriptHeader;
scriptHeader = gettext('-- This script was generated by the Schema Diff utility in pgAdmin 4. \n');
scriptHeader += gettext('-- For the circular dependencies, the order in which Schema Diff writes the objects is not very sophisticated \n');
scriptHeader += gettext('-- and may require manual changes to the script to ensure changes are applied in the correct order.\n');
scriptHeader += gettext('-- Please report an issue for any failure with the reproduction steps. \n');
if (selectedIds.length > 0) {
let script_array = { 1: [], 2: [], 3: [], 4: [], 5: [] },
script_body = '';
getGenerateScriptData(rows, selectedIds, script_array);
generatedScript = generateFinalScript(script_array, scriptHeader, script_body);
openQueryTool({ sid: sid, did: did, generatedScript: generatedScript, scriptHeader: scriptHeader });
} else {
openQueryTool({ sid: sid, did: did, scriptHeader: scriptHeader });
}
};
function openQueryTool({ sid, did, generatedScript, scriptHeader }) {
let baseServerUrl = url_for('schema_diff.get_server', { 'sid': sid, 'did': did });
schemaDiffToolContext.api({
url: baseServerUrl,
method: 'GET',
dataType: 'json',
contentType: 'application/json',
})
.then(function (res) {
let data = res.data.data;
let serverData = {};
if (data) {
let sqlId = `schema${params.transId}`;
serverData['sgid'] = data.gid;
serverData['sid'] = data.sid;
serverData['stype'] = data.type;
serverData['server'] = data.name;
serverData['user'] = data.user;
serverData['did'] = did;
serverData['database'] = data.database;
serverData['sql_id'] = sqlId;
if (_.isUndefined(generatedScript)) {
generatedScript = scriptHeader + 'BEGIN;' + '\n' + '' + '\n' + 'END;';
}
localStorage.setItem(sqlId, generatedScript);
generateScript(serverData, pgWindow.pgAdmin.Tools.SQLEditor);
setLoaderText(null);
}
})
.catch(function (xhr) {
setLoaderText(null);
raiseErrorOnFail(gettext('Generate script error'), xhr);
});
}
function generateGridData(record, tempData, allRowIds, filterParams) {
if (record.group_name in tempData && record.label in tempData[record.group_name]['children']) {
let chidId = record.id;
allRowIds.push(`${chidId}`);
tempData[record.group_name]['children'][record.label]['children'].push({
'id': chidId,
'parentId': tempData[record.group_name]['children'][record.label].id,
'label': record.title,
'status': record.status,
'isVisible': filterParams.includes(record.status) ? true : false,
'icon': `icon-${record.type}`,
'isExpanded': false,
'selected': false,
'oid': record.oid,
'itemType': record.type,
'source_oid': record.source_oid,
'target_oid': record.target_oid,
'source_scid': record.source_scid,
'target_scid': record.target_scid,
'dependenciesOid': record.dependencies.map(({ oid }) => oid),
'dependencies': record.dependencies,
'dependencieRowIds': [],
'ddlData': {
'SQLdiff': record.diff_ddl,
'sourceSQL': record.source_ddl,
'targetSQL': record.target_ddl
}
});
} else if (record.group_name in tempData) {
let chidId = Math.floor(Math.random() * 1000000);
allRowIds.push(`${chidId}`);
let subChildId = record.id;
allRowIds.push(`${subChildId}`);
tempData[record.group_name]['children'][record.label] = {
'id': chidId,
'parentId': tempData[record.group_name]['id'],
'label': record.label,
'identicalCount': 0,
'differentCount': 0,
'sourceOnlyCount': 0,
'targetOnlyCount': 0,
'icon': `icon-coll-${record.type}`,
'isExpanded': false,
'selected': false,
'children': [{
'id': subChildId,
'parentId': chidId,
'label': record.title,
'status': record.status,
'isVisible': filterParams.includes(record.status) ? true : false,
'icon': `icon-${record.type}`,
'isExpanded': false,
'selected': false,
'oid': record.oid,
'itemType': record.type,
'source_oid': record.source_oid,
'target_oid': record.target_oid,
'source_scid': record.source_scid,
'target_scid': record.target_scid,
'dependenciesOid': record.dependencies.map(({ oid }) => oid),
'dependencies': record.dependencies,
'dependencieRowIds': [],
'ddlData': {
'SQLdiff': record.diff_ddl,
'sourceSQL': record.source_ddl,
'targetSQL': record.target_ddl
}
}]
};
} else {
let label = record.label;
let _id = Math.floor(Math.random() * 100000);
let _subChildId = Math.floor(Math.random() * 100000);
allRowIds.push(`${_id}`);
allRowIds.push(`${_subChildId}`);
tempData[record.group_name] = {
'id': _id,
'label': record.group_name,
'icon': record.group_name == 'Database Objects' ? 'icon-coll-database' : 'icon-schema',
'groupType': record.group_name,
'isExpanded': false,
'selected': false,
'children': {}
};
let ch_id = record.id;
allRowIds.push(`${ch_id}`);
tempData[record.group_name]['children'][label] = {
'id': _subChildId,
'parentId': _id,
'label': record.label,
'identicalCount': 0,
'differentCount': 0,
'sourceOnlyCount': 0,
'targetOnlyCount': 0,
'selected': false,
'icon': `icon-coll-${record.type}`,
'isExpanded': false,
'children': [{
'id': ch_id,
'parentId': _subChildId,
'label': record.title,
'status': record.status,
'selected': false,
'itemType': record.type,
'isVisible': filterParams.includes(record.status) ? true : false,
'icon': `icon-${record.type}`,
'isExpanded': false,
'oid': record.oid,
'source_oid': record.source_oid,
'target_oid': record.target_oid,
'source_scid': record.source_scid,
'target_scid': record.target_scid,
'dependenciesOid': record.dependencies.map(({ oid }) => oid),
'dependencies': record.dependencies,
'dependencieRowIds': [],
'ddlData': {
'SQLdiff': record.diff_ddl,
'sourceSQL': record.source_ddl,
'targetSQL': record.target_ddl
}
}]
};
}
}
function getResultGridData(gridDataList, filterParams) {
let tempData = {};
let allRowIds = [];
setApiResult(gridDataList);
gridDataList.map((record) => {
generateGridData(record, tempData, allRowIds, filterParams);
});
let keyList = Object.keys(tempData);
let temp = [];
let rowDependencies = {};
for (let i = 0; i < keyList.length; i++) {
tempData[keyList[i]]['children'] = Object.values(tempData[keyList[i]]['children']);
let subChildList = [];
tempData[keyList[i]]['children'].map((ch) => ch.children.map(({ id }) => subChildList.push(`${id}`)));
tempData[keyList[i]]['metadata'] = {
isRoot: true,
children: tempData[keyList[i]]['children'].map(({ id }) => `${id}`),
subChildren: subChildList,
};
tempData[keyList[i]]['children'].map((child) => {
child['metadata'] = {
parentId: tempData[keyList[i]].id,
children: tempData[keyList[i]]['children'].map(({ id }) => `${id}`),
subChildren: child.children.map(({ id }) => `${id}`),
dependencies: [],
};
child.children.map((ch) => {
if (ch.dependenciesOid.length > 0) {
tempData[keyList[i]]['children'].map((el) => {
el.children.map((data) => {
if (ch.dependenciesOid.includes(data.oid)) {
ch.dependencieRowIds.push(`${data.id}`);
}
});
});
}
ch['metadata'] = {
parentId: child.id,
rootId: tempData[keyList[i]].id,
children: child.children.map(({ id }) => `${id}`),
};
child['metadata']['dependencies'].push(...ch.dependencieRowIds);
});
});
temp.push(tempData[keyList[i]]);
}
setRowDep(rowDependencies);
setShowResultGrid(true);
setGridData(temp);
setAllRowIdList([...new Set(allRowIds)]);
}
const getCompareStatus = () => {
let url_params = { 'trans_id': params.transId };
schemaDiffToolContext.api.get(url_for('schema_diff.poll', url_params)).then((res) => {
let msg = res.data.data.compare_msg;
if (res.data.data.diff_percentage != 100) {
msg = msg + gettext(` (this may take a few minutes)... ${res.data.data.diff_percentage} %`);
setLoaderText(msg);
}
})
.catch((err) => {
Notifier.error(gettext(err.message));
});
};
const connectDatabase = (sid, selectedDB, diff_type, databaseList) => {
schemaDiffToolContext.api({
method: 'POST',
url: url_for('schema_diff.connect_database', { 'sid': sid, 'did': selectedDB }),
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}).then((res) => {
let dbList = databaseList;
for (const opt of dbList) {
if (opt.value == selectedDB) {
opt.connected = true;
opt.image = res.data.data.icon || 'pg-icon-database';
getSchemaList(sid, selectedDB, diff_type);
if (diff_type == TYPE.SOURCE) {
setSelectedSourceDid(selectedDB);
setSourceDatabaseList(dbList);
} else {
setSelectedTargetDid(selectedDB);
setTargetDatabaseList(dbList);
}
break;
}
}
}).catch((error) => {
Notifier.error(gettext(`Error in connect database ${error.response.data}`));
});
};
const connectServer = (sid, diff_type, formData = null, serverList = []) => {
try {
schemaDiffToolContext.api({
method: 'POST',
url: url_for('schema_diff.connect_server', { 'sid': sid }),
data: formData,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}).then((res) => {
for (const group of serverList) {
for (const opt of group.options) {
if (opt.value == sid) {
opt.connected = true;
opt.image = res.data.data.icon || 'icon-pg';
break;
}
}
}
if (diff_type == TYPE.SOURCE) {
setSelectedSourceSid(sid);
} else {
setSelectedTargetSid(sid);
}
setSourceGroupServerList(serverList);
getDatabaseList(sid, diff_type);
}).catch((error) => {
showConnectServer(error.response?.data.result, sid, diff_type, serverList);
});
} catch (error) {
Notifier.error(gettext(`Error in connect server ${error.response.data}` ));
}
};
function getDatabaseList(sid, diff_type) {
schemaDiffToolContext.api.get(
url_for('schema_diff.databases', { 'sid': sid })
).then((res) => {
res.data.data.map((opt) => {
if (opt.is_maintenance_db) {
if (diff_type == TYPE.SOURCE) {
setSelectedSourceDid(opt.value);
} else {
setSelectedTargetDid(opt.value);
}
getSchemaList(sid, opt.value, diff_type);
}
});
if (diff_type == TYPE.SOURCE) {
setSourceDatabaseList(res.data.data);
} else {
setTargetDatabaseList(res.data.data);
}
});
}
function getSchemaList(sid, did, diff_type) {
schemaDiffToolContext.api.get(
url_for('schema_diff.schemas', { 'sid': sid, 'did': did })
).then((res) => {
if (diff_type == TYPE.SOURCE) {
setSourceSchemaList(res.data.data);
} else {
setTargetSchemaList(res.data.data);
}
});
}
function showConnectServer(result, sid, diff_type, serverList) {
schemaDiffToolContext.modal.showModal(gettext('Connect to server'), (closeModal) => {
return (
<ConnectServerContent
closeModal={() => {
closeModal();
}}
data={result}
onOK={(formData) => {
connectServer(sid, diff_type, formData, serverList);
}}
/>
);
});
}
function getFilterParams() {
let opt = [];
if(isInit && filterOptions.length == 0) {
opt = [FILTER_NAME.DIFFERENT, FILTER_NAME.SOURCE_ONLY, FILTER_NAME.TARGET_ONLY];
} else if(filterOptions.length > 0 ) {
opt = filterOptions;
}
return opt;
}
return (
<>
<Loader message={loaderText} style={{fontWeight: 900}}></Loader>
<Box id='compare-container-schema-diff'>
<Grid
container
direction="row"
alignItems="center"
key={_.uniqueId('c')}
>
<Grid item lg={7} md={7} sm={10} xs={10} key={_.uniqueId('c')}>
<InputComponent
label={gettext('Select Source')}
serverList={sourceGroupServerList}
databaseList={sourceDatabaseList}
schemaList={sourceSchemaList}
selectedSid={selectedSourceSid}
selectedDid={selectedSourceDid}
selectedScid={selectedSourceScid}
diff_type={TYPE.SOURCE}
></InputComponent>
</Grid>
<Grid item lg={5} md={5} sm={2} xs={2} key={_.uniqueId('c')} className={classes.helpBtn}>
<PgButtonGroup size="small">
<PgIconButton data-test='schema-diff-help' title={gettext('Help')} icon={<HelpIcon />} onClick={onHelpClick} />
</PgButtonGroup>
</Grid>
</Grid>
<Grid
container
direction="row"
alignItems="center"
key={_.uniqueId('c')}
>
<Grid item lg={7} md={7} sm={10} xs={10} key={_.uniqueId('c')}>
<InputComponent
label={gettext('Select Target')}
serverList={sourceGroupServerList}
databaseList={targetDatabaseList}
schemaList={targetSchemaList}
selectedSid={selectedTargetSid}
selectedDid={selectedTargetDid}
selectedScid={selectedTargetScid}
diff_type={TYPE.TARGET}
></InputComponent>
</Grid>
<Grid item lg={5} md={5} sm={12} xs={12} key={_.uniqueId('c')} className={classes.diffBtn}>
<SchemaDiffButtonComponent
sourceData={{
'sid': selectedSourceSid,
'did': selectedSourceDid,
'scid': selectedSourceScid,
}}
selectedRowIds={selectedRowIds}
rows={apiResult}
targetData={{
'sid': selectedTargetSid,
'did': selectedTargetDid,
'scid': selectedTargetScid,
}}
filterParams={getFilterParams()}
compareParams={compareOptions}
></SchemaDiffButtonComponent>
</Grid>
</Grid>
</Box>
{showResultGrid && gridData.length > 0 && selectedTargetDid && selectedSourceDid ?
<ResultGridComponent
gridData={gridData}
allRowIds={allRowIdList}
filterParams={filterOptions}
selectedRowIds={(rows) => { setSelectedRowIds(rows); }}
rowDependencies={rowDep}
transId={params.transId}
sourceData={{
'sid': selectedSourceSid,
'did': selectedSourceDid,
'scid': selectedSourceScid,
}}
targetData={{
'sid': selectedTargetSid,
'did': selectedTargetDid,
'scid': selectedTargetScid,
}}
></ResultGridComponent>
:
<Box className={classes.note}>
<InfoRoundedIcon style={{ fontSize: '1.2rem' }} />
{gettext(' Source and Target database server must be of the same major version.')}<br />
<strong>{gettext(' Database Compare:')}</strong>
{gettext(' Select the server and database for the source and target and Click')} <strong>{gettext('Compare.')}</strong>
<br />
<strong>{gettext('Schema Compare:')}</strong>
{gettext(' Select the server, database and schema for the source and target and Click')} <strong>{gettext('Compare.')}</strong>
<br />
<strong>{gettext('Note:')}</strong> {gettext('The dependencies will not be resolved in the Schema comparison.')}
</Box>
}
</>
);
}
SchemaDiffCompare.propTypes = {
params: PropTypes.object,
'params.transId': PropTypes.number,
};

View File

@ -0,0 +1,124 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { createContext, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import {DividerBox} from 'rc-dock';
import url_for from 'sources/url_for';
import { Box, makeStyles } from '@material-ui/core';
import { Results } from './Results';
import { SchemaDiffCompare } from './SchemaDiffCompare';
import EventBus from '../../../../../static/js/helpers/EventBus';
import getApiInstance from '../../../../../static/js/api_instance';
import { useModal } from '../../../../../static/js/helpers/ModalProvider';
export const SchemaDiffEventsContext = createContext();
export const SchemaDiffContext = createContext();
const useStyles = makeStyles((theme) => ({
resetRoot: {
padding: '2px 4px',
display: 'flex',
alignItems: 'center',
gap: '4px',
backgroundColor: theme.otherVars.editorToolbarBg,
flexWrap: 'wrap',
...theme.mixins.panelBorder.bottom,
'& #id-schema-diff': {
overflow: 'auto'
},
'& #id-results': {
overflow: 'auto'
}
},
resultPanle: {
backgroundColor: theme.palette.default.main,
zIndex: 5,
border: '1px solid ' + theme.otherVars.borderColor,
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
height: '50%',
minHeight: 150,
overflow: 'hidden',
},
comparePanel:{
overflow: 'hidden',
border: '1px solid ' + theme.otherVars.borderColor,
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
minHeight: 150,
height: '50%',
}
}));
export default function SchemaDiffComponent({params}) {
const classes = useStyles();
const eventBus = useRef(new EventBus());
const containerRef = React.useRef(null);
const api = getApiInstance();
const modal = useModal();
const [schemaDiffState, setSchemaDiffState] = useState({
preferences: null
});
const schemaDiffContextValue = useMemo(()=> ({
api: api,
modal: modal,
preferences_schema_diff: schemaDiffState.preferences
}), [schemaDiffState.preferences]);
registerUnload();
useEffect(() => {
reflectPreferences();
params.pgAdmin.Browser.onPreferencesChange('schema_diff', function () {
reflectPreferences();
});
}, []);
const reflectPreferences = useCallback(() => {
setSchemaDiffState({
preferences: params.pgAdmin.Browser.get_preferences_for_module('schema_diff')
});
}, []);
function registerUnload() {
window.addEventListener('unload', ()=>{
api.delete(url_for('schema_diff.close', {
trans_id: params.transId
}));
});
}
return (
<SchemaDiffContext.Provider value={schemaDiffContextValue}>
<SchemaDiffEventsContext.Provider value={eventBus.current}>
<Box display="flex" flexDirection="column" flexGrow="1" tabIndex="0" style={{overflowY: 'auto', minHeight: 80}}>
<DividerBox mode='vertical' style={{flexGrow: 1}}>
<div className={classes.comparePanel} id="schema-diff-compare-container" ref={containerRef}><SchemaDiffCompare params={params} /></div>
<div className={classes.resultPanle} id="schema-diff-result-container">
<Results />
</div>
</DividerBox>
</Box>
</SchemaDiffEventsContext.Provider>
</SchemaDiffContext.Provider>
);
}
SchemaDiffComponent.propTypes = {
params: PropTypes.object
};

View File

@ -0,0 +1,22 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import pgAdmin from 'sources/pgadmin';
import pgBrowser from 'top/browser/static/js/browser';
import SchemaDiff from './SchemaDiffModule';
if (!pgAdmin.Tools) {
pgAdmin.Tools = {};
}
pgAdmin.Tools.SchemaDiff = SchemaDiff.getInstance(pgAdmin, pgBrowser);
module.exports = {
SchemaDiff: SchemaDiff,
};

View File

@ -1,543 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import $ from 'jquery';
import Backbone from 'backbone';
import Backform from 'pgadmin.backform';
import gettext from 'sources/gettext';
import clipboard from 'sources/selection/clipboard';
var formatNode = function (opt) {
if (!opt.id) {
return opt.text;
}
var optimage = $(opt.element).data('image');
if (!optimage) {
return opt.text;
} else {
return $('<span></span>').append(
$('<span></span>', {
class: 'wcTabIcon ' + optimage,
})
).append($('<span></span>').text(opt.text));
}
};
let SchemaDiffSqlControl =
Backform.SqlFieldControl.extend({
defaults: {
label: '',
extraClasses: [], // Add default control height
helpMessage: null,
maxlength: 4096,
rows: undefined,
copyRequired: false,
},
template: _.template([
'<% if (copyRequired) { %><button class="btn btn-secondary ddl-copy d-none">' + gettext('Copy') + '</button> <% } %>',
'<div class="pgadmin-controls pg-el-9 pg-el-12 sql_field_layout <%=extraClasses.join(\' \')%>">',
' <textarea ',
' class="<%=Backform.controlClassName%> " name="<%=name%>" aria-label="<%=name%>"',
' maxlength="<%=maxlength%>" placeholder="<%-placeholder%>" <%=disabled ? "disabled" : ""%>',
' rows=<%=rows%>',
' <%=required ? "required" : ""%>><%-value%></textarea>',
' <% if (helpMessage && helpMessage.length) { %>',
' <span class="<%=Backform.helpMessageClassName%>"><%=helpMessage%></span>',
' <% } %>',
'</div>',
].join('\n')),
initialize: function() {
Backform.TextareaControl.prototype.initialize.apply(this, arguments);
this.sqlCtrl = null;
_.bindAll(this, 'onFocus', 'onBlur', 'refreshTextArea', 'copyData');
},
render: function() {
let obj = Backform.SqlFieldControl.prototype.render.apply(this, arguments);
obj.sqlCtrl.setOption('readOnly', true);
if(this.$el.find('.ddl-copy')) this.$el.find('.ddl-copy').on('click', this.copyData);
return obj;
},
copyData() {
event.stopPropagation();
clipboard.copyTextToClipboard(this.model.get('diff_ddl'));
this.$el.find('.ddl-copy').text(gettext('Copied!'));
var self = this;
setTimeout(function() {
let $copy = self.$el.find('.ddl-copy');
if (!$copy.hasClass('d-none')) $copy.addClass('d-none');
$copy.text(gettext('Copy'));
}, 3000);
return false;
},
onFocus: function() {
let $ctrl = this.$el.find('.pgadmin-controls').first(),
$copy = this.$el.find('.ddl-copy');
if (!$ctrl.hasClass('focused')) $ctrl.addClass('focused');
if ($copy.hasClass('d-none')) $copy.removeClass('d-none');
},
});
let SchemaDiffSelect2Control =
Backform.Select2Control.extend({
defaults: _.extend(Backform.Select2Control.prototype.defaults, {
url: undefined,
transform: undefined,
url_with_id: false,
select2: {
allowClear: true,
placeholder: gettext('Select an item...'),
width: 'style',
templateResult: formatNode,
templateSelection: formatNode,
},
controlsClassName: 'pgadmin-controls pg-el-sm-11 pg-el-12',
}),
className: function() {
return 'pgadmin-controls pg-el-sm-4';
},
events: {
'focus select': 'clearInvalid',
'keydown :input': 'processTab',
'select2:select': 'onSelect',
'select2:selecting': 'beforeSelect',
'select2:clear': 'onChange',
},
template: _.template([
'<% if(label == false) {} else {%>',
' <label class="<%=Backform.controlLabelClassName%>"><%=label%></label>',
'<% }%>',
'<div class="<%=controlsClassName%>">',
' <select class="<%=Backform.controlClassName%> <%=extraClasses.join(\' \')%>"',
' name="<%=name%>" aria-label="<%=name%>" value="<%-value%>" <%=disabled ? "disabled" : ""%>',
' <%=required ? "required" : ""%><%= select2.multiple ? " multiple>" : ">" %>',
' <%=select2.first_empty ? " <option></option>" : ""%>',
' <% for (var i=0; i < options.length; i++) {%>',
' <% if (options[i].group) { %>',
' <% var group = options[i].group; %>',
' <% if (options[i].optval) { %> <% var option_length = options[i].optval.length; %>',
' <optgroup label="<%=group%>">',
' <% for (var subindex=0; subindex < option_length; subindex++) {%>',
' <% var option = options[i].optval[subindex]; %>',
' <option ',
' <% if (option.image) { %> data-image=<%=option.image%> <%}%>',
' <% if (option.connected) { %> data-connected=connected <%}%>',
' value=<%- formatter.fromRaw(option.value) %>',
' <% if (option.selected) {%>selected="selected"<%} else {%>',
' <% if (!select2.multiple && option.value === rawValue) {%>selected="selected"<%}%>',
' <% if (select2.multiple && rawValue && rawValue.indexOf(option.value) != -1){%>selected="selected" data-index="rawValue.indexOf(option.value)"<%}%>',
' <%}%>',
' <%= disabled ? "disabled" : ""%>><%-option.label%></option>',
' <%}%>',
' </optgroup>',
' <%}%>',
' <%} else {%>',
' <% var option = options[i]; %>',
' <option ',
' <% if (option.image) { %> data-image=<%=option.image%> <%}%>',
' <% if (option.connected) { %> data-connected=connected <%}%>',
' value=<%- formatter.fromRaw(option.value) %>',
' <% if (option.selected) {%>selected="selected"<%} else {%>',
' <% if (!select2.multiple && option.value === rawValue) {%>selected="selected"<%}%>',
' <% if (select2.multiple && rawValue && rawValue.indexOf(option.value) != -1){%>selected="selected" data-index="rawValue.indexOf(option.value)"<%}%>',
' <%}%>',
' <%= disabled ? "disabled" : ""%>><%-option.label%></option>',
' <%}%>',
' <%}%>',
' </select>',
' <% if (helpMessage && helpMessage.length) { %>',
' <span class="<%=Backform.helpMessageClassName%>"><%=helpMessage%></span>',
' <% } %>',
'</div>',
].join('\n')),
beforeSelect: function() {
var selVal = arguments[0].params.args.data.id;
if(this.field.get('connect') && this.$el.find('option[value="'+selVal+'"]').attr('data-connected') !== 'connected') {
this.field.get('connect').apply(this, [selVal, this.changeIcon.bind(this)]);
} else {
$(this.$sel).trigger('change');
setTimeout(function(){ this.onChange.apply(this); }.bind(this), 200);
}
},
changeIcon: function(data) {
let span = this.$el.find('.select2-selection .select2-selection__rendered span.wcTabIcon'),
selSpan = this.$el.find('option:selected');
if (span.hasClass('icon-server-not-connected') || span.hasClass('icon-shared-server-not-connected')) {
let icon = (data.icon) ? data.icon : 'icon-pg';
span.removeClass('icon-server-not-connected');
span.addClass(icon);
span.attr('data-connected', 'connected');
selSpan.data().image = icon;
selSpan.attr('data-connected', 'connected');
this.onChange.apply(this);
}
else if (span.hasClass('icon-database-not-connected')) {
let icon = (data.icon) ? data.icon : 'pg-icon-database';
span.removeClass('icon-database-not-connected');
span.addClass(icon);
span.attr('data-connected', 'connected');
selSpan.removeClass('icon-database-not-connected');
selSpan.data().image = icon;
selSpan.attr('data-connected', 'connected');
this.onChange.apply(this);
}
},
onChange: function() {
var model = this.model,
attrArr = this.field.get('name').split('.'),
name = attrArr.shift(),
path = attrArr.join('.'),
value = this.getValueFromDOM(),
changes = {},
that = this;
if (this.model.errorModel instanceof Backbone.Model) {
if (_.isEmpty(path)) {
this.model.errorModel.unset(name);
} else {
var nestedError = this.model.errorModel.get(name);
if (nestedError) {
this.keyPathSetter(nestedError, path, null);
this.model.errorModel.set(name, nestedError);
}
}
}
changes[name] = _.isEmpty(path) ? value : _.clone(model.get(name)) || {};
if (!_.isEmpty(path)) that.keyPathSetter(changes[name], path, value);
that.stopListening(that.model, 'change:' + name, that.render);
model.set(changes);
that.listenTo(that.model, 'change:' + name, that.render);
},
render: function() {
/*
* Initialization from the original control.
*/
this.fetchData();
return Backform.Select2Control.prototype.render.apply(this, arguments);
},
fetchData: function() {
/*
* We're about to fetch the options required for this control.
*/
var self = this,
url = self.field.get('url'),
m = self.model;
url = _.isFunction(url) ? url.apply(m) : url;
if (url && self.field.get('deps')) {
url = url.replace('sid', m.get(self.field.get('deps')[0]));
}
// Hmm - we found the url option.
// That means - we needs to fetch the options from that node.
if (url) {
var data;
m.trigger('pgadmin:view:fetching', m, self.field);
$.ajax({
async: false,
url: url,
})
.done(function(res) {
/*
* We will cache this data for short period of time for avoiding
* same calls.
*/
data = res.data;
})
.fail(function() {
m.trigger('pgadmin:view:fetch:error', m, self.field);
});
m.trigger('pgadmin:view:fetched', m, self.field);
// To fetch only options from cache, we do not need time from 'at'
// attribute but only options.
//
/*
* Transform the data
*/
var transform = this.field.get('transform') || self.defaults.transform;
if (transform && _.isFunction(transform)) {
// We will transform the data later, when rendering.
// It will allow us to generate different data based on the
// dependencies.
self.field.set('options', transform.bind(self, data));
} else {
self.field.set('options', data);
}
}
},
});
let SchemaDiffHeaderView = Backform.Form.extend({
label: '',
className: function() {
return 'pg-el-sm-12 pg-el-md-12 pg-el-lg-12 pg-el-12';
},
tabPanelClassName: function() {
return Backform.tabClassName;
},
tabIndex: 0,
initialize: function(opts) {
this.label = opts.label;
Backform.Form.prototype.initialize.apply(this, arguments);
},
template: _.template(`
<div class="row pgadmin-control-group">
<div class="col-1 control-label">` + gettext('Select Source') + `</div>
<div class="col-6 source row"></div>
</div>
<div class="row pgadmin-control-group">
<div class="col-1 control-label">` + gettext('Select Target') + `</div>
<div class="col-6 target row"></div>
<div class="col-5 target-buttons">
<div class="action-btns d-flex">
<div class="btn-group mr-auto" role="group" aria-label="">
<button class="btn btn-primary"><span class="pg-font-icon icon-compare sql-icon-lg"></span>&nbsp;` + gettext('Compare') + `</button>
<button id="btn-ignore-dropdown" type="button" class="btn btn-primary-icon dropdown-toggle dropdown-toggle-split mr-auto"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" aria-label="ignore"
title=""
tabindex="0">
</button>` +
[
'<ul class="dropdown-menu ignore">',
'<li>',
'<a class="dropdown-item" id="btn-ignore-owner" href="#" tabindex="0">',
'<i class="fa fa-check visibility-hidden" aria-hidden="true"></i>',
'<span> ' + gettext('Ignore owner') + ' </span>',
'</a>',
'</li>',
'<li>',
'<a class="dropdown-item" id="btn-ignore-whitespaces" href="#" tabindex="0">',
'<i class="fa fa-check visibility-hidden" aria-hidden="true"></i>',
'<span> ' + gettext('Ignore whitespace') + ' </span>',
'</a>',
'</li>',
'</ul>',
].join('\n') + `</div>
<button id="generate-script" class="btn btn-primary-icon mr-1" disabled><i class="fa fa-file-code sql-icon-lg"></i>&nbsp;` + gettext('Generate Script') + `</button>
<div class="btn-group mr-1" role="group" aria-label="">
<button id="btn-filter" type="button" class="btn btn-primary-icon"
title=""
tabindex="0"
style="pointer-events: none;">
<i class="fa fa-filter sql-icon-lg" aria-hidden="true"></i>&nbsp;` + gettext('Filter') + `
</button>
<button id="btn-filter-dropdown" type="button" class="btn btn-primary-icon dropdown-toggle dropdown-toggle-split"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" aria-label="filter"
title=""
tabindex="0">
</button>` +
[
'<ul class="dropdown-menu filter">',
'<li>',
'<a class="dropdown-item" id="btn-identical" href="#" tabindex="0">',
'<i class="identical fa fa-check visibility-hidden" aria-hidden="true"></i>',
'<span> ' + gettext('Identical') + ' </span>',
'</a>',
'</li>',
'<li>',
'<a class="dropdown-item" id="btn-differentt" href="#" tabindex="0">',
'<i class="different fa fa-check" aria-hidden="true"></i>',
'<span> ' + gettext('Different') + ' </span>',
'</a>',
'</li>',
'<li>',
'<a class="dropdown-item" id="btn-source-only" href="#" tabindex="0">',
'<i class="source-only fa fa-check" aria-hidden="true"></i>',
'<span> ' + gettext('Source Only') + ' </span>',
'</a>',
'</li>',
'<li>',
'<a class="dropdown-item" id="btn-target-only" href="#" tabindex="0">',
'<i class="target-only fa fa-check" aria-hidden="true"></i>',
'<span> ' + gettext('Target Only') + ' </span>',
'</a>',
'</li>',
'</ul>',
'</div>',
'</div>',
'</div>',
'</div>',
].join('\n')
),
render: function() {
this.cleanup();
var controls = this.controls,
m = this.model,
self = this,
idx = (this.tabIndex * 100);
this.$el.empty();
$(this.template()).appendTo(this.$el);
this.fields.each(function(f) {
var cntr = new(f.get('control'))({
field: f,
model: m,
dialog: self,
tabIndex: idx,
});
if (f.get('group') && f.get('group') == 'source') {
self.$el.find('.source').append(cntr.render().$el);
}
else {
self.$el.find('.target').append(cntr.render().$el);
}
controls.push(cntr);
});
return this;
},
remove: function(opts) {
if (opts && opts.data) {
if (this.model) {
if (this.model.reset) {
this.model.reset({
validate: false,
silent: true,
stop: true,
});
}
this.model.clear({
validate: false,
silent: true,
stop: true,
});
delete(this.model);
}
if (this.errorModel) {
this.errorModel.clear({
validate: false,
silent: true,
stop: true,
});
delete(this.errorModel);
}
}
this.cleanup();
Backform.Form.prototype.remove.apply(this, arguments);
},
});
let SchemaDiffFooterView = Backform.Form.extend({
className: function() {
return 'set-group pg-el-12';
},
tabPanelClassName: function() {
return Backform.tabClassName;
},
legendClass: 'badge',
contentClass: Backform.accordianContentClassName,
template: {
'content': _.template(`
<div class="pg-el-sm-12 row <%=contentClass%>">
<div class="pg-el-sm-4 ddl-source">` + gettext('Source') + `</div>
<div class="pg-el-sm-4 ddl-target">` + gettext('Target') + `</div>
<div class="pg-el-sm-4 ddl-diff">` + gettext('Difference') + `
</div>
</div>
</div>
`),
},
initialize: function(opts) {
this.label = opts.label;
Backform.Form.prototype.initialize.apply(this, arguments);
},
render: function() {
this.cleanup();
let m = this.model,
$el = this.$el,
tmpl = this.template,
controls = this.controls,
data = {
'className': _.result(this, 'className'),
'legendClass': _.result(this, 'legendClass'),
'contentClass': _.result(this, 'contentClass'),
'collapse': _.result(this, 'collapse'),
},
idx = (this.tabIndex * 100);
this.$el.empty();
let el = $((tmpl['content'])(data)).appendTo($el);
this.fields.each(function(f) {
let cntr = new(f.get('control'))({
field: f,
model: m,
dialog: self,
tabIndex: idx,
name: f.get('name'),
});
if (f.get('group') && f.get('group') == 'ddl-source') {
el.find('.ddl-source').append(cntr.render().$el);
}
else if (f.get('group') && f.get('group') == 'ddl-target') {
el.find('.ddl-target').append(cntr.render().$el);
}
else {
el.find('.ddl-diff').append(cntr.render().$el);
}
controls.push(cntr);
});
$('div.CodeMirror div textarea').attr('aria-label', 'textarea');
let $diff_sc = this.$el.find('.source_ddl'),
$diff_tr = this.$el.find('.target_ddl'),
$diff = this.$el.find('.diff_ddl'),
footer_height = this.$el.parent().height() - 50;
$diff_sc.height(footer_height);
$diff_sc.css({
'height': footer_height + 'px',
});
$diff_tr.height(footer_height);
$diff_tr.css({
'height': footer_height + 'px',
});
$diff.height(footer_height);
$diff.css({
'height': footer_height + 'px',
});
return this;
},
});
export {
SchemaDiffSelect2Control,
SchemaDiffHeaderView,
SchemaDiffFooterView,
SchemaDiffSqlControl,
};

View File

@ -1,171 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
define('pgadmin.schemadiff', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'sources/csrf', 'pgadmin.alertifyjs', 'sources/utils', 'pgadmin.browser.node',
], function(
gettext, url_for, $, _, pgAdmin, csrfToken, Alertify, commonUtils,
) {
var wcDocker = window.wcDocker,
pgBrowser = pgAdmin.Browser;
/* Return back, this has been called more than once */
if (pgBrowser.SchemaDiff)
return pgBrowser.SchemaDiff;
// Create an Object Restore of pgBrowser class
pgBrowser.SchemaDiff = {
init: function() {
if (this.initialized)
return;
this.initialized = true;
csrfToken.setPGCSRFToken(pgAdmin.csrf_token_header, pgAdmin.csrf_token);
// Define the nodes on which the menus to be appear
var menus = [{
name: 'schema_diff',
module: this,
applies: ['tools'],
callback: 'show_schema_diff_tool',
priority: 1,
label: gettext('Schema Diff'),
enable: true,
below: true,
}];
pgBrowser.add_menus(menus);
// Creating a new pgBrowser frame to show the data.
var schemaDiffFrameType = new pgBrowser.Frame({
name: 'frm_schemadiff',
showTitle: true,
isCloseable: true,
isPrivate: true,
url: 'about:blank',
});
let self = this;
/* Cache may take time to load for the first time
* Keep trying till available
*/
let cacheIntervalId = setInterval(function() {
if(pgBrowser.preference_version() > 0) {
self.preferences = pgBrowser.get_preferences_for_module('schema_diff');
clearInterval(cacheIntervalId);
}
},0);
pgBrowser.onPreferencesChange('schema_diff', function() {
self.preferences = pgBrowser.get_preferences_for_module('schema_diff');
});
// Load the newly created frame
schemaDiffFrameType.load(pgBrowser.docker);
return this;
},
// Callback to draw schema diff for objects
show_schema_diff_tool: function() {
var self = this,
baseUrl = url_for('schema_diff.initialize', null);
$.ajax({
url: baseUrl,
method: 'GET',
dataType: 'json',
contentType: 'application/json',
})
.done(function(res) {
self.trans_id = res.data.schemaDiffTransId;
res.data.panel_title = gettext('Schema Diff'); //TODO: Set the panel title
// TODO: Following function is used to test the fetching of the
// databases this should be moved to server selection event later.
self.launch_schema_diff(res.data);
})
.fail(function(xhr) {
self.raise_error_on_fail(gettext('Schema Diff initialize error') , xhr);
});
},
launch_schema_diff: function(data) {
var panel_title = data.panel_title,
trans_id = data.schemaDiffTransId,
panel_tooltip = '';
var url_params = {
'trans_id': trans_id,
'editor_title': panel_title,
},
baseUrl = url_for('schema_diff.panel', url_params);
var browser_preferences = pgBrowser.get_preferences_for_module('browser');
var open_new_tab = browser_preferences.new_browser_tab_open;
if (open_new_tab && open_new_tab.includes('schema_diff')) {
window.open(baseUrl, '_blank');
// Send the signal to runtime, so that proper zoom level will be set.
setTimeout(function() {
pgBrowser.send_signal_to_runtime('Runtime new window opened');
}, 500);
} else {
var propertiesPanel = pgBrowser.docker.findPanels('properties'),
schemaDiffPanel = pgBrowser.docker.addPanel('frm_schemadiff', wcDocker.DOCK.STACKED, propertiesPanel[0]);
commonUtils.registerDetachEvent(schemaDiffPanel);
// Rename schema diff tab
schemaDiffPanel.on(wcDocker.EVENT.RENAME, function(panel_data) {
Alertify.prompt('', panel_data.$titleText[0].textContent,
// We will execute this function when user clicks on the OK button
function(evt, value) {
if(value) {
// Remove the leading and trailing white spaces.
value = value.trim();
schemaDiffPanel.title('<span>'+ _.escape(value) +'</span>');
}
},
// We will execute this function when user clicks on the Cancel
// button. Do nothing just close it.
function(evt) { evt.cancel = false; }
).set({'title': gettext('Rename Panel')});
});
// Set panel title and icon
schemaDiffPanel.title('<span title="'+panel_tooltip+'">'+panel_title+'</span>');
schemaDiffPanel.icon('pg-font-icon icon-compare');
schemaDiffPanel.focus();
var openSchemaDiffURL = function(j) {
// add spinner element
$(j).data('embeddedFrame').$container.append(pgBrowser.SchemaDiff.spinner_el);
setTimeout(function() {
var frameInitialized = $(j).data('frameInitialized');
if (frameInitialized) {
var frame = $(j).data('embeddedFrame');
if (frame) {
frame.openURL(baseUrl);
frame.$container.find('.pg-sp-container').delay(1000).hide(1);
}
} else {
openSchemaDiffURL(j);
}
}, 100);
};
openSchemaDiffURL(schemaDiffPanel);
}
},
};
return pgBrowser.SchemaDiff;
});

View File

@ -1,243 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
function handleDependencies() {
event.stopPropagation();
let isChecked = event.target.checked || (event.target.checked === undefined &&
event.target.className && event.target.className.indexOf('unchecked') == -1);
let isHeaderSelected = false;
if (event.target.id !== undefined) isHeaderSelected = event.target.id.includes('header-selector');
if (this.gridContext && this.gridContext.rowIndex && _.isUndefined(this.gridContext.row.rows)) {
// Single Row Selection
let rowData = this.grid.getData().getItem(this.gridContext.rowIndex);
this.gridContext = {};
if (rowData.status) {
let depRows = this.selectDependencies(rowData, isChecked);
this.selectedRowCount = this.grid.getSelectedRows().length;
if (isChecked && depRows.length > 0)
this.grid.setSelectedRows(depRows);
else if (!isChecked)
this.grid.setSelectedRows(this.grid.getSelectedRows().filter(x => !depRows.includes(x)));
this.ddlCompare(rowData);
}
} else if((this.gridContext && this.gridContext.row && !_.isUndefined(this.gridContext.row.rows)) ||
this.selectedRowCount != this.grid.getSelectedRows().length) {
// Group / All Rows Selection
this.selectedRowCount = this.grid.getSelectedRows().length;
if (this.gridContext.row && this.gridContext.row.__group) {
let context = this.gridContext;
this.gridContext = {};
this.selectDependenciesForGroup(isChecked, context);
} else {
this.gridContext = {};
this.selectDependenciesForAll(isChecked, isHeaderSelected);
}
}
if (this.grid.getSelectedRows().length > 0) {
this.header.$el.find('button#generate-script').removeAttr('disabled');
} else {
this.header.$el.find('button#generate-script').attr('disabled', true);
}
}
function selectDependenciesForGroup(isChecked, context) {
let self = this,
finalRows = [];
if (!isChecked) {
_.each(context.row.rows, function(row) {
if (row && row.status && row.status.toLowerCase() != 'identical' ) {
let d = self.selectDependencies(row, isChecked);
finalRows = finalRows.concat(d);
}
});
}
else {
_.each(self.grid.getSelectedRows(), function(row) {
let data = self.grid.getData().getItem(row);
if (data.status && data.status.toLowerCase() != 'identical') {
finalRows = finalRows.concat(self.selectDependencies(data, isChecked));
}
});
}
finalRows = [...new Set(finalRows)];
if (isChecked)
self.grid.setSelectedRows(finalRows);
else {
let filterRows = [];
filterRows = self.grid.getSelectedRows().filter(x => !finalRows.includes(x));
self.selectedRowCount = filterRows.length;
self.grid.setSelectedRows(filterRows);
}
}
function selectDependenciesForAll(isChecked, isHeaderSelected) {
let self = this,
finalRows = [];
if(!isChecked && isHeaderSelected) {
self.dataView.getItems().map(function(el) {
el.dependentCount = [];
el.dependLevel = 1;
});
self.selectedRowCount = 0;
return;
}
_.each(self.grid.getSelectedRows(), function(row) {
let data = self.grid.getData().getItem(row);
if (data.status) {
finalRows = finalRows.concat(self.selectDependencies(data, isChecked));
}
});
finalRows = [...new Set(finalRows)];
if (isChecked && finalRows.length > 0)
self.grid.setSelectedRows(finalRows);
else if (!isChecked) {
let filterRows = [];
filterRows = self.grid.getSelectedRows().filter(x => !finalRows.includes(x));
self.selectedRowCount = filterRows.length;
self.grid.setSelectedRows(filterRows);
}
}
function selectDependencies(data, isChecked) {
let self = this,
rows = [],
setDependencies = undefined,
setOrigDependencies = undefined,
finalRows = [];
if (!data.dependLevel || !isChecked) data.dependLevel = 1;
if (!data.dependentCount || !isChecked) data.dependentCount = [];
if (data.status && data.status.toLowerCase() == 'identical') {
self.selectedRowCount = self.grid.getSelectedRows().length;
return [];
}
setDependencies = function(rowData, dependencies, is_checked) {
// Special handling for extension, if extension is present in the
// dependency list then iterate and select only extension node.
let extensions = dependencies.filter(item => item.type == 'extension');
if (extensions.length > 0) dependencies = extensions;
_.each(dependencies, function(dependency) {
if (dependency.length == 0) return;
let dependencyData = [];
dependencyData = self.dataView.getItems().filter(item => item.type == dependency.type && item.oid == dependency.oid);
if (dependencyData.length > 0) {
dependencyData = dependencyData[0];
if (!dependencyData.dependentCount) dependencyData.dependentCount = [];
let groupData = [];
if (dependencyData.status && dependencyData.status.toLowerCase() != 'identical') {
groupData = self.dataView.getGroups().find(
(item) => { if (dependencyData.group_name == item.groupingKey) return item.groups; }
);
if (groupData && groupData.groups) {
groupData = groupData.groups.find(
(item) => { return item.groupingKey == dependencyData.group_name + ':|:' + dependencyData.type; }
);
if (groupData && groupData.collapsed == 1)
self.dataView.expandGroup(dependencyData.group_name + ':|:' + dependencyData.type);
}
if (is_checked || _.isUndefined(is_checked)) {
dependencyData.dependLevel = rowData.dependLevel + 1;
if (dependencyData.dependentCount.indexOf(rowData.oid) === -1)
dependencyData.dependentCount.push(rowData.oid);
rows[rows.length] = dependencyData;
} else {
dependencyData.dependentCount.splice(dependencyData.dependentCount.indexOf(rowData.oid), 1);
if (dependencyData.dependentCount.length == 0) {
rows[rows.length] = dependencyData;
dependencyData.dependLevel = 1;
}
}
}
if (Object.keys(dependencyData.dependencies).length > 0) {
if (dependencyData.dependentCount.indexOf(rowData.oid) !== -1 ) {
let depCirRows = dependencyData.dependencies.filter(x => x.oid !== rowData.oid);
if (!dependencyData.orig_dependencies)
dependencyData.orig_dependencies = Object.assign([], dependencyData.dependencies);
dependencyData.dependencies = depCirRows;
}
setDependencies(dependencyData, dependencyData.dependencies, is_checked);
}
}
});
};
setDependencies(data, data.dependencies, isChecked);
setOrigDependencies = function(dependencies) {
_.each(dependencies, function(dependency) {
if (dependency.length == 0) return;
let dependencyData = [];
dependencyData = self.dataView.getItems().filter(item => item.type == dependency.type && item.oid == dependency.oid);
if (dependencyData.length > 0) {
dependencyData = dependencyData[0];
if (dependencyData.orig_dependencies && Object.keys(dependencyData.orig_dependencies).length > 0) {
if (!dependencyData.dependentCount) dependencyData.dependentCount = [];
if (dependencyData.status && dependencyData.status.toLowerCase() != 'identical') {
dependencyData.dependencies = dependencyData.orig_dependencies;
dependencyData.orig_dependencies = [];
}
if (dependencyData.dependencies.length > 0) {
setOrigDependencies(dependencyData.dependencies);
}
}
}
});
};
setOrigDependencies(data.dependencies);
if (isChecked) finalRows = self.grid.getSelectedRows();
_.each(rows, function(row) {
let r = self.grid.getData().getRowByItem(row);
if(!_.isUndefined(r) && finalRows.indexOf(r) === -1 ) {
finalRows.push(self.grid.getData().getRowByItem(row));
}
});
self.selectedRowCount = finalRows.length;
return finalRows;
}
export {
handleDependencies,
selectDependenciesForGroup,
selectDependenciesForAll,
selectDependencies,
};

View File

@ -1,38 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
define([
'sources/url_for', 'jquery',
'sources/pgadmin', 'pgadmin.tools.schema_diff_ui',
], function(
url_for, $, pgAdmin, SchemaDiffUIModule
) {
var pgTools = pgAdmin.Tools = pgAdmin.Tools || {};
var SchemaDiffUI = SchemaDiffUIModule.default;
/* Return back, this has been called more than once */
if (pgTools.SchemaDiffHook)
return pgTools.SchemaDiffHook;
pgTools.SchemaDiffHook = {
load: function(trans_id) {
window.onbeforeunload = function() {
$.ajax({
url: url_for('schema_diff.close', {'trans_id': trans_id}),
method: 'DELETE',
});
};
let schemaUi = new SchemaDiffUI($('#schema-diff-container'), trans_id);
schemaUi.render();
},
};
return pgTools.SchemaDiffHook;
});

View File

@ -1,967 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import url_for from 'sources/url_for';
import $ from 'jquery';
import gettext from 'sources/gettext';
import Alertify from 'pgadmin.alertifyjs';
import Backbone from 'backbone';
import Slick from 'sources/../bundle/slickgrid';
import pgAdmin from 'sources/pgadmin';
import {setPGCSRFToken} from 'sources/csrf';
import 'pgadmin.tools.sqleditor';
import pgWindow from 'sources/window';
import _ from 'underscore';
import Notify from '../../../../static/js/helpers/Notifier';
import { showSchemaDiffServerPassword } from '../../../../static/js/Dialogs/index';
import { SchemaDiffSelect2Control, SchemaDiffHeaderView,
SchemaDiffFooterView, SchemaDiffSqlControl} from './schema_diff.backform';
import { handleDependencies, selectDependenciesForGroup,
selectDependenciesForAll, selectDependencies } from './schema_diff_dependency';
import { generateScript } from '../../../sqleditor/static/js/show_query_tool';
var wcDocker = window.wcDocker;
export default class SchemaDiffUI {
constructor(container, trans_id) {
var self = this;
this.$container = container;
this.header = null;
this.trans_id = trans_id;
this.filters = ['Identical', 'Different', 'Source Only', 'Target Only'];
this.sel_filters = ['Different', 'Source Only', 'Target Only'];
this.ignore_filters = ['owner', 'whitespaces'];
this.ignore_whitespaces = 0;
this.ignore_owner = 0;
this.dataView = null;
this.grid = null;
this.selection = {};
this.model = new Backbone.Model({
source_sid: undefined,
source_did: undefined,
source_scid: undefined,
target_sid: undefined,
target_did: undefined,
target_scid: undefined,
source_ddl: undefined,
target_ddl: undefined,
diff_ddl: undefined,
});
setPGCSRFToken(pgAdmin.csrf_token_header, pgAdmin.csrf_token);
this.docker = new wcDocker(
this.$container, {
allowContextMenu: false,
allowCollapse: false,
loadingClass: 'pg-sp-icon',
themePath: url_for('static', {
'filename': 'css',
}),
theme: 'webcabin.overrides.css',
}
);
this.header_panel = new pgAdmin.Browser.Panel({
name: 'schema_diff_header_panel',
showTitle: false,
isCloseable: false,
isPrivate: true,
content: '<div id="schema-diff-header" class="pg-el-container" el="sm"></div><div id="schema-diff-grid" class="pg-el-container" el="sm"></div>',
elContainer: true,
});
this.footer_panel = new pgAdmin.Browser.Panel({
name: 'schema_diff_footer_panel',
title: gettext('DDL Comparison'),
isCloseable: false,
isPrivate: true,
height: '60',
content: `<div id="schema-diff-ddl-comp" class="pg-el-container" el="sm">
<div id="ddl_comp_fetching_data" class="pg-sp-container schema-diff-busy-fetching d-none">
<div class="pg-sp-content">
<div class="row">
<div class="col-12 pg-sp-icon"></div>
</div>
<div class="row"><div class="col-12 pg-sp-text">` + gettext('Comparing objects...') + `</div></div>
</div>
</div></div>`,
});
this.header_panel.load(this.docker);
this.footer_panel.load(this.docker);
this.panel_obj = this.docker.addPanel('schema_diff_header_panel', wcDocker.DOCK.TOP, {w:'95%', h:'50%'});
this.footer_panel_obj = this.docker.addPanel('schema_diff_footer_panel', wcDocker.DOCK.BOTTOM, this.panel_obj, {w:'95%', h:'50%'});
self.footer_panel_obj.on(wcDocker.EVENT.VISIBILITY_CHANGED, function() {
setTimeout(function() {
this.resize_grid();
}.bind(self), 200);
});
self.footer_panel_obj.on(wcDocker.EVENT.RESIZE_ENDED, function() {
setTimeout(function() {
this.resize_panels();
}.bind(self), 200);
});
}
raise_error_on_fail(alert_title, xhr) {
try {
if (_.isUndefined(xhr.responseText)) {
Notify.alert(alert_title, gettext('Unable to get the response text.'));
} else {
var err = JSON.parse(xhr.responseText);
Notify.alert(alert_title, err.errormsg);
}
} catch (e) {
Notify.alert(alert_title, gettext(e.message));
}
}
resize_panels() {
let $src_ddl = $('#schema-diff-ddl-comp .source_ddl'),
$tar_ddl = $('#schema-diff-ddl-comp .target_ddl'),
$diff_ddl = $('#schema-diff-ddl-comp .diff_ddl'),
footer_height = $('#schema-diff-ddl-comp').height() - 50;
$src_ddl.height(footer_height);
$src_ddl.css({
'height': footer_height + 'px',
});
$tar_ddl.height(footer_height);
$tar_ddl.css({
'height': footer_height + 'px',
});
$diff_ddl.height(footer_height);
$diff_ddl.css({
'height': footer_height + 'px',
});
this.resize_grid();
}
compare_schemas() {
var self = this,
url_params = self.model.toJSON();
if (url_params['source_sid'] == '' || _.isUndefined(url_params['source_sid']) ||
url_params['source_did'] == '' || _.isUndefined(url_params['source_did']) ||
url_params['target_sid'] == '' || _.isUndefined(url_params['target_sid']) ||
url_params['target_did'] == '' || _.isUndefined(url_params['target_did'])
) {
Notify.alert(gettext('Selection Error'), gettext('Please select source and target.'));
return false;
}
// Check if user has selected the same options for comparison on the GUI
let opts = [['source_sid', 'target_sid'], ['source_did', 'target_did'], ['source_scid', 'target_scid']];
let isSameOptsSelected = true;
for (let opt of opts) {
if (url_params[opt[0]] && url_params[opt[1]] &&
(parseInt(url_params[opt[0]]) !== parseInt(url_params[opt[1]]))) {
isSameOptsSelected = false;
}
}
if (isSameOptsSelected) {
Notify.alert(gettext('Selection Error'), gettext('Please select the different source and target.'));
return false;
}
this.selection = JSON.parse(JSON.stringify(url_params));
url_params['trans_id'] = self.trans_id;
url_params['ignore_owner'] = self.ignore_owner;
url_params['ignore_whitespaces'] = self.ignore_whitespaces;
_.each(url_params, function(key, val) {
url_params[key] = parseInt(val, 10);
});
var baseUrl = url_for('schema_diff.compare_database', url_params);
// If compare two schema then change the base url
if (url_params['source_scid'] != '' && !_.isUndefined(url_params['source_scid']) &&
url_params['target_scid'] != '' && !_.isUndefined(url_params['target_scid'])) {
baseUrl = url_for('schema_diff.compare_schema', url_params);
}
self.model.set({
'source_ddl': undefined,
'target_ddl': undefined,
'diff_ddl': undefined,
});
self.render_grid([]);
self.footer.render();
self.startDiffPoller();
return $.ajax({
url: baseUrl,
method: 'GET',
dataType: 'json',
contentType: 'application/json',
})
.done(function (res) {
self.stopDiffPoller();
self.render_grid(res.data);
})
.fail(function (xhr) {
self.raise_error_on_fail(gettext('Schema compare error'), xhr);
self.stopDiffPoller();
});
}
generate_script() {
var self = this,
baseServerUrl = url_for('schema_diff.get_server', {'sid': self.selection['target_sid'],
'did': self.selection['target_did']}),
sel_rows = self.grid ? self.grid.getSelectedRows() : [],
url_params = self.selection,
generated_script = undefined,
open_query_tool,
script_header;
script_header = gettext('-- This script was generated by the Schema Diff utility in pgAdmin 4. \n');
script_header += gettext('-- For the circular dependencies, the order in which Schema Diff writes the objects is not very sophisticated \n');
script_header += gettext('-- and may require manual changes to the script to ensure changes are applied in the correct order.\n');
script_header += gettext('-- Please report an issue for any failure with the reproduction steps. \n');
_.each(url_params, function(key, val) {
url_params[key] = parseInt(val, 10);
});
$('#diff_fetching_data').removeClass('d-none');
$('#diff_fetching_data').find('.schema-diff-busy-text').text('Generating script...');
open_query_tool = function get_server_details() {
$.ajax({
url: baseServerUrl,
method: 'GET',
dataType: 'json',
contentType: 'application/json',
})
.done(function (res) {
let data = res.data;
let server_data = {};
if (data) {
let sqlId = `schema${self.trans_id}`;
server_data['sgid'] = data.gid;
server_data['sid'] = data.sid;
server_data['stype'] = data.type;
server_data['server'] = data.name;
server_data['user'] = data.user;
server_data['did'] = self.model.get('target_did');
server_data['database'] = data.database;
server_data['sql_id'] = sqlId;
if (_.isUndefined(generated_script)) {
generated_script = script_header + 'BEGIN;' + '\n' + self.model.get('diff_ddl') + '\n' + 'END;';
}
localStorage.setItem(sqlId, generated_script);
generateScript(server_data, pgWindow.pgAdmin.Tools.SQLEditor, Alertify);
}
$('#diff_fetching_data').find('.schema-diff-busy-text').text('');
$('#diff_fetching_data').addClass('d-none');
})
.fail(function (xhr) {
self.raise_error_on_fail(gettext('Generate script error'), xhr);
$('#diff_fetching_data').find('.schema-diff-busy-text').text('');
$('#diff_fetching_data').addClass('d-none');
});
};
if (sel_rows.length > 0) {
let script_array = {1: [], 2: [], 3: [], 4: [], 5: []},
script_body = '';
for (let sel_row_val of sel_rows) {
let data = self.grid.getData().getItem(sel_row_val);
if(!_.isUndefined(data.diff_ddl)) {
if (!(data.dependLevel in script_array)) script_array[data.dependLevel] = [];
// Check whether the selected object belongs to source only schema
// if yes then we will have to add create schema statement before
// creating any other object.
if (!_.isUndefined(data.source_schema_name) && !_.isNull(data.source_schema_name)) {
let schema_query = '\nCREATE SCHEMA IF NOT EXISTS ' + data.source_schema_name + ';\n';
if (script_array[data.dependLevel].indexOf(schema_query) == -1) {
script_array[data.dependLevel].push(schema_query);
}
}
script_array[data.dependLevel].push(data.diff_ddl);
}
}
_.each(Object.keys(script_array).reverse(), function(s) {
if (script_array[s].length > 0) {
script_body += script_array[s].join('\n') + '\n\n';
}
});
generated_script = script_header + 'BEGIN;' + '\n' + script_body + 'END;';
open_query_tool();
} else if (!_.isUndefined(self.model.get('diff_ddl'))) {
open_query_tool();
}
return false;
}
check_empty_diff() {
var self = this;
this.panel_obj.$container.find('#schema-diff-grid .slick-viewport .pg-panel-message').remove();
if (self.dataView.getFilteredItems().length == 0) {
let msg = gettext('No difference found');
// Make the height to 0px to avoid extra scroll bar, height will be calculated automatically.
this.panel_obj.$container.find('#schema-diff-grid .slick-viewport .grid-canvas')[0].style.height = '0px';
this.panel_obj.$container.find('#schema-diff-grid .slick-viewport'
).prepend('<div class="pg-panel-message">'+ msg +'</div>');
}
}
render_grid(data) {
var self = this;
var grid;
if (self.grid) {
// Only render the data
self.render_grid_data(data);
self.check_empty_diff();
return;
}
// Checkbox Column
var checkboxSelector = new Slick.CheckboxSelectColumn({
cssClass: 'slick-cell-checkboxsel',
minWidth: 30,
});
// Format Schema object title with appropriate icon
var formatColumnTitle = function (row, cell, value, columnDef, dataContext) {
let icon = 'icon-' + dataContext.type;
return '<i class="ml-2 wcTabIcon '+ icon +'"></i><span>' + value + '</span>';
};
// Grid Columns
var grid_width = (self.grid_width - 47) / 2 ;
var columns = [
checkboxSelector.getColumnDefinition(),
{id: 'title', name: gettext('Objects'), field: 'title', minWidth: grid_width, formatter: formatColumnTitle},
{id: 'status', name: gettext('Comparison Result'), field: 'status', minWidth: grid_width},
{id: 'label', name: gettext('Objects'), field: 'label', width: 0, minWidth: 0, maxWidth: 0,
cssClass: 'really-hidden', headerCssClass: 'really-hidden'},
{id: 'type', name: gettext('Objects'), field: 'type', width: 0, minWidth: 0, maxWidth: 0,
cssClass: 'really-hidden', headerCssClass: 'really-hidden'},
{id: 'id', name: 'id', field: 'id', width: 0, minWidth: 0, maxWidth: 0,
cssClass: 'really-hidden', headerCssClass: 'really-hidden' },
];
// Grid Options
var options = {
enableCellNavigation: true,
enableColumnReorder: false,
enableRowSelection: true,
};
// Grouping by Schema Object
self.groupBySchemaObject = function() {
self.dataView.setGrouping([{
getter: 'group_name',
formatter: function (g) {
let icon = 'icon-schema';
if (g.rows[0].group_name == 'Database Objects'){
icon = 'icon-coll-database';
}
return '<i class="wcTabIcon '+ icon +'"></i><span>' + _.escape(g.rows[0].group_name);
},
aggregateCollapsed: true,
lazyTotalsCalculation: true,
}, {
getter: 'type',
formatter: function (g) {
let icon = 'icon-coll-' + g.value;
let identical=0, different=0, source_only=0, target_only=0;
for (let row_val of g.rows) {
if (row_val['status'] == self.filters[0]) identical++;
else if (row_val['status'] == self.filters[1]) different++;
else if (row_val['status'] == self.filters[2]) source_only++;
else if (row_val['status'] == self.filters[3]) target_only++;
}
return '<i class="wcTabIcon '+ icon +'"></i><span>' + g.rows[0].label + ' - ' + gettext('Identical') + ': <strong>' + identical + '</strong>&nbsp;&nbsp;' + gettext('Different') + ': <strong>' + different + '</strong>&nbsp;&nbsp;' + gettext('Source Only') + ': <strong>' + source_only + '</strong>&nbsp;&nbsp;' + gettext('Target Only') + ': <strong>' + target_only + '</strong></span>';
},
aggregateCollapsed: true,
collapsed: true,
lazyTotalsCalculation: true,
}]);
};
var groupItemMetadataProvider = new Slick.Data.GroupItemMetadataProvider({ checkboxSelect: true,
checkboxSelectPlugin: checkboxSelector });
// Dataview for grid
self.dataView = new Slick.Data.DataView({
groupItemMetadataProvider: groupItemMetadataProvider,
inlineFilters: false,
});
// Wire up model events to drive the grid
self.dataView.onRowCountChanged.subscribe(function () {
grid.updateRowCount();
grid.render();
self.accessibility_error();
});
self.dataView.onRowsChanged.subscribe(function (e, args) {
grid.invalidateRows(args.rows);
grid.render();
self.accessibility_error();
});
// Change Row css on the basis of item status
self.dataView.getItemMetadata = function(row) {
let item = self.dataView.getItem(row),
group_item = groupItemMetadataProvider.getGroupRowMetadata(item);
if (item.__group) {
return group_item;
}
if(item.status === 'Different') {
return { cssClasses: 'different' };
} else if (item.status === 'Source Only') {
return { cssClasses: 'source' };
} else if (item.status === 'Target Only') {
return { cssClasses: 'target' };
}
return null;
};
// Grid filter
self.filter = function (item) {
let self_local = this;
if (self_local.sel_filters.indexOf(item.status) !== -1) return true;
return false;
};
let $data_grid = $('#schema-diff-grid');
grid = this.grid = new Slick.Grid($data_grid, self.dataView, columns, options);
$('label[for='+ columns[0].name.split('\'')[1] +']').append('<span style="display:none">checkbox</span>');
grid.registerPlugin(groupItemMetadataProvider);
grid.setSelectionModel(new Slick.RowSelectionModel({selectActiveRow: false}));
grid.registerPlugin(checkboxSelector);
self.dataView.syncGridSelection(grid, true, true);
grid.onMouseEnter.subscribe(function (evt) {
var cell = grid.getCellFromEvent(evt);
self.gridContext = {};
self.gridContext.rowIndex = cell.row;
self.gridContext.row = grid.getDataItem(cell.row);
}.bind(self));
grid.onMouseLeave.subscribe(function () {
self.gridContext = {};
});
grid.onClick.subscribe(function(e, args) {
if (args.row) {
data = args.grid.getData().getItem(args.row);
if (data.status) this.ddlCompare(data);
}
}.bind(self));
grid.onSelectedRowsChanged.subscribe(self.handleDependencies.bind(this));
self.model.on('change:diff_ddl', function(event) {
self.handleDependencies.bind(event, self);
});
$('#schema-diff-grid').on('keyup', function() {
if ((event.keyCode == 38 || event.keyCode ==40) && this.grid.getActiveCell().row) {
data = this.grid.getData().getItem(this.grid.getActiveCell().row);
this.ddlCompare(data);
}
}.bind(self));
self.render_grid_data(data);
}
render_grid_data(data) {
var self = this;
self.grid.setSelectedRows([]);
self.selected_row_count = self.grid.getSelectedRows().length;
data.sort((a, b) => (a.label > b.label) ? 1 : (a.label === b.label) ? ((a.title > b.title) ? 1 : -1) : -1);
self.dataView.beginUpdate();
self.dataView.setItems(data);
self.dataView.setFilter(self.filter.bind(self));
self.groupBySchemaObject();
self.dataView.endUpdate();
self.dataView.refresh();
self.resize_grid();
self.accessibility_error();
}
accessibility_error() {
$('.slick-viewport').scroll(function() {
setTimeout(function() {
$('span.slick-column-name label').append('<span style="display:none">checkbox</span>');
$('div.slick-cell.l0 label').each(function(inx, el) {
$(el).append('<span style="display:none">checkbox</span>');
});
}, 0);
});
}
handle_generate_button(){
if (this.grid.getSelectedRows().length > 0 || (this.model.get('diff_ddl') != '' && !_.isUndefined(this.model.get('diff_ddl')))) {
this.header.$el.find('button#generate-script').removeAttr('disabled');
} else {
this.header.$el.find('button#generate-script').attr('disabled', true);
}
}
resize_grid() {
let $data_grid = $('#schema-diff-grid'),
grid_height = (this.panel_obj.height() > 0) ? this.panel_obj.height() - 100 : this.grid_height - 100;
$data_grid.height(grid_height);
$data_grid.css({
'height': grid_height + 'px',
});
if (this.grid) this.grid.resizeCanvas();
}
getCompareStatus() {
var self = this,
url_params = {'trans_id': self.trans_id},
baseUrl = url_for('schema_diff.poll', url_params);
$.ajax({
url: baseUrl,
method: 'GET',
dataType: 'json',
contentType: 'application/json',
})
.done(function (res) {
let msg = _.escape(res.data.compare_msg);
if (res.data.diff_percentage != 100) {
msg = msg + gettext(' (this may take a few minutes)...');
}
msg = msg + '<br>' + gettext('%s completed.', res.data.diff_percentage + '%');
$('#diff_fetching_data').find('.schema-diff-busy-text').html(msg);
})
.fail(function (xhr) {
self.raise_error_on_fail(gettext('Poll error'), xhr);
self.stopDiffPoller('fail');
});
}
startDiffPoller() {
$('#ddl_comp_fetching_data').addClass('d-none');
$('#diff_fetching_data').removeClass('d-none');
/* Execute once for the first time as setInterval will not do */
this.getCompareStatus();
this.diff_poller_int_id = setInterval(this.getCompareStatus.bind(this), 1000);
}
stopDiffPoller(status) {
clearInterval(this.diff_poller_int_id);
// The last polling for comparison
if (status !== 'fail') this.getCompareStatus();
$('#diff_fetching_data').find('.schema-diff-busy-text').text('');
$('#diff_fetching_data').addClass('d-none');
}
ddlCompare(data) {
var self = this,
node_type = data.type,
source_oid = data.oid,
target_oid;
self.model.set({
'source_ddl': undefined,
'target_ddl': undefined,
'diff_ddl': undefined,
}, {silent: true});
if(data.status && data.status.toLowerCase() == 'identical') {
var url_params = self.selection;
target_oid = data.target_oid;
url_params['trans_id'] = self.trans_id;
url_params['source_scid'] = data.source_scid;
url_params['target_scid'] = data.target_scid;
url_params['source_oid'] = source_oid;
url_params['target_oid'] = target_oid;
url_params['comp_status'] = data.status;
url_params['node_type'] = node_type;
_.each(url_params, function(key, val) {
url_params[key] = parseInt(val, 10);
});
$('#ddl_comp_fetching_data').removeClass('d-none');
var baseUrl = url_for('schema_diff.ddl_compare', url_params);
self.model.url = baseUrl;
self.model.fetch({
success: function() {
self.footer.render();
$('#ddl_comp_fetching_data').addClass('d-none');
},
error: function() {
self.footer.render();
$('#ddl_comp_fetching_data').addClass('d-none');
},
});
} else {
self.model.set({
'source_ddl': data.source_ddl,
'target_ddl': data.target_ddl,
'diff_ddl': data.diff_ddl,
}, {silent: true});
self.footer.render();
}
}
transformFunc(data) {
let group_template_options = [];
for (let key in data) {
if (data.hasOwnProperty(key)) {
group_template_options.push({'group': key, 'optval': data[key]});
}
}
return group_template_options;
}
render() {
let self = this;
let panel = self.docker.findPanels('schema_diff_header_panel')[0];
var header = panel.$container.find('#schema-diff-header');
self.header = new SchemaDiffHeaderView({
el: header,
model: this.model,
fields: [{
name: 'source_sid', label: false,
control: SchemaDiffSelect2Control,
transform: function(data) {
return self.transformFunc(data);
},
url: url_for('schema_diff.servers'),
select2: {
allowClear: true,
placeholder: gettext('Select server...'),
},
connect: function() {
self.connect_server(arguments[0], arguments[1]);
},
group: 'source',
disabled: function() {
return false;
},
}, {
name: 'source_did',
group: 'source',
deps: ['source_sid'],
control: SchemaDiffSelect2Control,
url: function() {
if (this.get('source_sid'))
return url_for('schema_diff.databases', {'sid': this.get('source_sid')});
return false;
},
select2: {
allowClear: true,
placeholder: gettext('Select database...'),
},
disabled: function(m) {
let self_local = this;
if (!_.isUndefined(m.get('source_sid')) && !_.isNull(m.get('source_sid'))
&& m.get('source_sid') !== '') {
setTimeout(function() {
for (let opt_val of self_local.options) {
if (opt_val.is_maintenance_db) {
m.set('source_did', opt_val.value);
}
}
}, 10);
return false;
}
setTimeout(function() {
m.set('source_did', undefined);
}, 10);
return true;
},
connect: function() {
self.connect_database(this.model.get('source_sid'), arguments[0], arguments[1]);
},
}, {
name: 'source_scid',
control: SchemaDiffSelect2Control,
group: 'source',
deps: ['source_sid', 'source_did'],
url: function() {
if (this.get('source_sid') && this.get('source_did'))
return url_for('schema_diff.schemas', {'sid': this.get('source_sid'), 'did': this.get('source_did')});
return false;
},
select2: {
allowClear: true,
placeholder: gettext('Select schema...'),
},
disabled: function(m) {
if (!_.isUndefined(m.get('source_did')) && !_.isNull(m.get('source_did'))
&& m.get('source_did') !== '') {
return false;
}
setTimeout(function() {
m.set('source_scid', undefined);
}, 10);
return true;
},
}, {
name: 'target_sid', label: false,
control: SchemaDiffSelect2Control,
transform: function(data) {
return self.transformFunc(data);
},
group: 'target',
url: url_for('schema_diff.servers'),
select2: {
allowClear: true,
placeholder: gettext('Select server...'),
},
disabled: function() {
return false;
},
connect: function() {
self.connect_server(arguments[0], arguments[1]);
},
}, {
name: 'target_did',
control: SchemaDiffSelect2Control,
group: 'target',
deps: ['target_sid'],
url: function() {
if (this.get('target_sid'))
return url_for('schema_diff.databases', {'sid': this.get('target_sid')});
return false;
},
select2: {
allowClear: true,
placeholder: gettext('Select database...'),
},
disabled: function(m) {
let self_local = this;
if (!_.isUndefined(m.get('target_sid')) && !_.isNull(m.get('target_sid'))
&& m.get('target_sid') !== '') {
setTimeout(function() {
for (let opt_val of self_local.options) {
if (opt_val.is_maintenance_db) {
m.set('target_did', opt_val.value);
}
}
}, 10);
return false;
}
setTimeout(function() {
m.set('target_did', undefined);
}, 10);
return true;
},
connect: function() {
self.connect_database(this.model.get('target_sid'), arguments[0], arguments[1]);
},
}, {
name: 'target_scid',
control: SchemaDiffSelect2Control,
group: 'target',
deps: ['target_sid', 'target_did'],
url: function() {
if (this.get('target_sid') && this.get('target_did'))
return url_for('schema_diff.schemas', {'sid': this.get('target_sid'), 'did': this.get('target_did')});
return false;
},
select2: {
allowClear: true,
placeholder: gettext('Select schema...'),
},
disabled: function(m) {
if (!_.isUndefined(m.get('target_did')) && !_.isNull(m.get('target_did'))
&& m.get('target_did') !== '') {
return false;
}
setTimeout(function() {
m.set('target_scid', undefined);
}, 10);
return true;
},
}],
});
self.footer = new SchemaDiffFooterView({
model: this.model,
fields: [{
name: 'source_ddl', label: false,
control: SchemaDiffSqlControl,
group: 'ddl-source',
}, {
name: 'target_ddl', label: false,
control: SchemaDiffSqlControl,
group: 'ddl-target',
}, {
name: 'diff_ddl', label: false,
control: SchemaDiffSqlControl,
group: 'ddl-diff', copyRequired: true,
}],
});
self.header.render();
self.header.$el.find('button.btn-primary').on('click', self.compare_schemas.bind(self));
self.header.$el.find('button#generate-script').on('click', self.generate_script.bind(self));
self.header.$el.find('ul.filter a.dropdown-item').on('click', self.refresh_filters.bind(self));
self.header.$el.find('ul.ignore a.dropdown-item').on('click', self.refresh_ignore_settings.bind(self));
/* Set the default value for 'ignore owner' and 'ignore whitespace' */
let pref = pgWindow.pgAdmin.Browser.get_preferences_for_module('schema_diff');
if (pref.ignore_owner) self.header.$el.find('ul.ignore a.dropdown-item#btn-ignore-owner').click();
if (pref.ignore_whitespaces) self.header.$el.find('ul.ignore a.dropdown-item#btn-ignore-whitespaces').click();
let footer_panel = self.docker.findPanels('schema_diff_footer_panel')[0],
header_panel = self.docker.findPanels('schema_diff_header_panel')[0];
footer_panel.$container.find('#schema-diff-ddl-comp').append(self.footer.render().$el);
$('div.CodeMirror div textarea').attr('aria-label', 'textarea');
header_panel.$container.find('#schema-diff-grid').append(`<div class='obj_properties container-fluid'>
<div class='pg-panel-message'>` + gettext('<strong>Database Compare:</strong> Select the server and database for the source and target and Click <strong>Compare</strong>.') +
gettext('</br><strong>Schema Compare:</strong> Select the server, database and schema for the source and target and Click <strong>Compare</strong>.') +
gettext('</br><strong>Note:</strong> The dependencies will not be resolved in the Schema comparison.') + '</div></div>');
self.grid_width = $('#schema-diff-grid').width();
self.grid_height = this.panel_obj.height();
}
refresh_filters(event) {
let self = this;
_.each(self.filters, function(filter) {
let index = self.sel_filters.indexOf(filter);
let filter_class = '.' + filter.replace(' ', '-').toLowerCase();
if ($(event.currentTarget).find(filter_class).length == 1) {
if ($(filter_class).hasClass('visibility-hidden') === true) {
$(filter_class).removeClass('visibility-hidden');
if (index === -1) self.sel_filters.push(filter);
} else {
$(filter_class).addClass('visibility-hidden');
if(index !== -1 ) self.sel_filters.splice(index, 1);
}
}
});
// Check whether comparison data is loaded or not
if(!_.isUndefined(self.dataView) && !_.isNull(self.dataView)) {
// Refresh the grid
self.dataView.refresh();
self.check_empty_diff();
}
}
refresh_ignore_settings(event) {
let self = this,
element = $(event.currentTarget).find('.fa-check');
if (element.length == 1) {
if (element.hasClass('visibility-hidden') === true) {
element.removeClass('visibility-hidden');
if (event.currentTarget.id === 'btn-ignore-owner') self.ignore_owner = 1;
if (event.currentTarget.id === 'btn-ignore-whitespaces') self.ignore_whitespaces = 1;
} else {
element.addClass('visibility-hidden');
if (event.currentTarget.id === 'btn-ignore-owner') self.ignore_owner = 0;
if (event.currentTarget.id === 'btn-ignore-whitespaces') self.ignore_whitespaces = 0;
}
}
}
connect_database(server_id, db_id, callback) {
var url = url_for('schema_diff.connect_database', {'sid': server_id, 'did': db_id});
$.post(url)
.done(function(res) {
if (res.success && res.data) {
callback(res.data);
}
})
.fail(function(xhr, error) {
Notify.pgNotifier(error, xhr, gettext('Failed to connect the database.'));
});
}
connect_server(server_id, callback) {
let self = this;
var onFailure = function(
xhr, status, error, sid, err_callback
) {
Notify.pgNotifier('error', xhr, error, function(msg) {
setTimeout(function() {
showSchemaDiffServerPassword(
self.docker,
gettext('Connect to Server'),
msg,
sid,
err_callback,
onSuccess,
onFailure
);
}, 100);
});
},
onSuccess = function(res, suc_callback) {
if (res && res.data) {
// We're not reconnecting
suc_callback(res.data);
}
};
var url = url_for('schema_diff.connect_server', {'sid': server_id});
$.post(url)
.done(function(res) {
if (res.success == 1) {
return onSuccess(res, callback);
}
})
.fail(function(xhr, status, error) {
return onFailure(
xhr, status, error, server_id, callback
);
});
}
}
SchemaDiffUI.prototype.handleDependencies = handleDependencies;
SchemaDiffUI.prototype.selectDependenciesForGroup = selectDependenciesForGroup;
SchemaDiffUI.prototype.selectDependenciesForAll = selectDependenciesForAll;
SchemaDiffUI.prototype.selectDependencies = selectDependencies;

View File

@ -1,145 +0,0 @@
#schema-diff-container {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: $color-gray-light;
}
#schema-diff-grid .slick-header .slick-header-columns {
background: $color-bg;
height: 40px;
border-bottom: none;
}
#schema-diff-grid .slick-header .slick-header-column.ui-state-default {
padding: 4px 0 3px 6px;
border-bottom: $panel-border;
border-right: $panel-border;
}
#schema-diff-grid {
font-family: $font-family-primary;
font-size: $tree-font-size;
.ui-widget-content {
background-color: $input-bg;
color: $input-color;
}
.ui-state-default {
color: $color-fg;
}
}
.slick-row:hover .slick-cell{
border-top: $table-hover-border;
border-bottom: $table-hover-border;
background-color: $table-hover-bg-color;
}
#schema-diff-grid .slick-header .slick-header-column.selected {
background-color: $color-primary;
color: $color-primary-fg;
}
.slick-row .slick-cell {
border-bottom: $panel-border;
border-right: $panel-border;
z-index: 0;
}
#schema-diff-grid .slick-row .slick-cell.l0.r0.selected {
background-color: inherit;
}
#schema-diff-grid .slick-row > .slick-cell.selected {
background-color: $table-hover-bg-color !important;
border-top: $table-hover-border;
border-bottom: $table-hover-border;
}
#schema-diff-grid div.slick-header.ui-state-default {
background: $color-bg;
border-bottom: none;
border-right: none;
border-top: $panel-border;
}
#schema-diff-grid .different {
background-color: $schemadiff-diff-row-color !important;
color: $schema-diff-color-fg;
}
#schema-diff-grid .source {
background-color: $schemadiff-source-row-color;
color: $schema-diff-color-fg;
}
#schema-diff-grid .target {
background-color: $schemadiff-target-row-color !important;
color: $schema-diff-color-fg;
}
#schema-diff-grid .slick-row.active {
background-color: $table-bg-selected !important;
color: $schema-diff-color-fg;
}
#schema-diff-ddl-comp {
height: 100%;
bottom: 10px;
background-color: $color-bg !important;
overflow-y: hidden;
}
#schema-diff-grid .slick-group-select-checkbox {
width: 13px;
height: 13px;
margin-right: 1rem;
vertical-align: middle;
display: inline-block;
}
.slick-group-toggle.collapsed::before {
font-family: $font-family-icon;
content: "\f054";
font-size: 0.6rem;
border: none;
font-weight: 900;
}
.slick-group-toggle.expanded::before {
font-family: $font-family-icon;
content: "\f078";
font-size: 0.6rem;
margin-left: 0rem;
font-weight: 900;
}
#schema-diff-ddl-comp .badge .caret::before {
font-family: $font-family-icon;
content: "\f078";
font-size: 0.7rem;
margin-left: 0rem;
font-weight: 900;
}
.slick-group {
color: $input-color !important;
}
.slick-group:hover {
color: $schema-diff-color-fg !important;
}
.slick-group.active {
color: $schema-diff-color-fg !important;
}
.select2-selection__placeholder {
color: $select2-placeholder !important;
}
#btn-ignore-dropdown {
color: $btn-primary-color !important;
background-color: $color-primary !important;
border-color: $color-primary !important;
}

View File

@ -2,14 +2,10 @@
{% block init_script %}
try {
require(
['sources/generated/slickgrid', 'sources/generated/codemirror', 'sources/generated/browser_nodes'],
['sources/generated/codemirror', 'sources/generated/browser_nodes', 'sources/generated/schema_diff'],
function() {
require(['sources/generated/schema_diff'], function(pgSchemaDiffHook) {
var pgSchemaDiffHook = pgSchemaDiffHook || pgAdmin.Tools.SchemaDiffHook;
pgSchemaDiffHook.load({{trans_id}});
}, function() {
console.log(arguments);
});
var pgSchemaDiff = window.pgAdmin.Tools.SchemaDiff;
pgSchemaDiff.load(document.getElementById('schema-diff-main-container'),{{trans_id}});
},
function() {
console.log(arguments);
@ -19,18 +15,28 @@ try {
}
{% endblock %}
{% block css_link %}
<style>
#schema-diff-main-container {
display: flex;
flex-direction: column;
height: 100%;
}
#schema-diff-main-container:not(:empty) + .pg-sp-container {
display: none;
}
</style>
<link type="text/css" rel="stylesheet" href="{{ url_for('browser.browser_css')}}"/>
{% endblock %}
{% block title %}{{editor_title}}{% endblock %}
{% block body %}
<div id="schema-diff-container">
<div id="diff_fetching_data" class="pg-sp-container schema-diff-busy-fetching d-none">
<div class="pg-sp-content">
<div class="row">
<div class="col-12 pg-sp-icon"></div>
</div>
<div class="row"><div class="col-12 pg-sp-text schema-diff-busy-text"></div></div>
<div id="schema-diff-main-container" tabindex="0">
<div class="pg-sp-container">
<div class="pg-sp-content">
<div class="row">
<div class="col-12 pg-sp-icon"></div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -130,7 +130,7 @@ const finaliseData = (nodeData, datum)=> {
datum.icon = 'icon-' + datum.type;
/* finalise path */
[datum.path, datum.id_path] = translateSearchObjectsPath(nodeData, datum.path, datum.catalog_level);
/* id is required by slickgrid dataview */
/* id is required by dataview */
datum.id = datum.id_path ? datum.id_path.join('.') : _.uniqueId(datum.name);
datum.other_info = datum.other_info ? _.escape(datum.other_info) : datum.other_info;
@ -423,4 +423,4 @@ export default function SearchObjects({nodeData}) {
SearchObjects.propTypes = {
onClose: PropTypes.func,
nodeData: PropTypes.object,
};
};

View File

@ -15,7 +15,6 @@ import * as commonUtils from 'sources/utils';
import $ from 'jquery';
import url_for from 'sources/url_for';
import _ from 'lodash';
import alertify from 'pgadmin.alertifyjs';
import pgWindow from 'sources/window';
import pgAdmin from 'sources/pgadmin';
import pgBrowser from 'pgadmin.browser';
@ -27,6 +26,7 @@ import ReactDOM from 'react-dom';
import QueryToolComponent from './components/QueryToolComponent';
import ModalProvider from '../../../../static/js/helpers/ModalProvider';
import Theme from '../../../../static/js/Theme';
import { showRenamePanel } from '../../../../static/js/Dialogs';
const wcDocker = window.wcDocker;
@ -242,38 +242,19 @@ export default class SQLEditor {
}
onPanelRename(queryToolPanel, panelData, is_query_tool) {
var temp_title = panelData.$titleText[0].textContent;
var is_dirty_editor = queryToolPanel.is_dirty_editor ? queryToolPanel.is_dirty_editor : false;
var title = queryToolPanel.is_dirty_editor ? panelData.$titleText[0].textContent.replace(/.$/, '') : temp_title;
alertify.prompt('', title,
// We will execute this function when user clicks on the OK button
function(evt, value) {
// Remove the leading and trailing white spaces.
value = value.trim();
if(value) {
var is_file = false;
if(panelData.$titleText[0].innerHTML.includes('File - ')) {
is_file = true;
}
var selected_item = pgBrowser.tree.selected();
var panel_titles = '';
if(is_query_tool) {
panel_titles = panelTitleFunc.getPanelTitle(pgBrowser, selected_item, value);
} else {
panel_titles = showViewData.generateViewDataTitle(pgBrowser, selected_item, value);
}
// Set title to the selected tab.
if (is_dirty_editor) {
panel_titles = panel_titles + ' *';
}
panelTitleFunc.setQueryToolDockerTitle(queryToolPanel, is_query_tool, _.unescape(panel_titles), is_file);
}
},
// We will execute this function when user clicks on the Cancel
// button. Do nothing just close it.
function(evt) { evt.cancel = false; }
).set({'title': gettext('Rename Panel')});
let preferences = pgBrowser.get_preferences_for_module('browser');
let temp_title = panelData.$titleText[0].textContent;
let is_dirty_editor = queryToolPanel.is_dirty_editor ? queryToolPanel.is_dirty_editor : false;
let title = queryToolPanel.is_dirty_editor ? panelData.$titleText[0].textContent.replace(/.$/, '') : temp_title;
let qtdata = {
is_query_tool: is_query_tool,
is_file: panelData.$titleText[0].innerHTML.includes('File - '),
is_dirty_editor: is_dirty_editor
};
showRenamePanel(title, preferences, queryToolPanel, 'querytool', qtdata);
}

View File

@ -32,7 +32,7 @@ class CheckForXssFeatureTest(BaseFeatureTest):
2) Properties Tab (BackFrom)
3) Dependents Tab (BackGrid)
4) SQL Tab (Code Mirror)
5) Query Tool (SlickGrid)
5) Query Tool (Result Grid)
"""
scenarios = [
@ -189,7 +189,7 @@ class CheckForXssFeatureTest(BaseFeatureTest):
def _check_xss_in_query_tool(self):
print(
"\n\tChecking the SlickGrid cell for XSS vulnerabilities",
"\n\tChecking the Result Grid cell for XSS vulnerabilities",
file=sys.stderr, end=""
)
self.page.fill_codemirror_area_with(
@ -205,7 +205,7 @@ class CheckForXssFeatureTest(BaseFeatureTest):
self._check_escaped_characters(
source_code,
'&lt;img src="x" onerror="console.log(1)"&gt;',
"Query tool (SlickGrid)"
"Query tool (Result Grid)"
)
def _check_xss_in_query_tool_history(self):
@ -255,7 +255,7 @@ class CheckForXssFeatureTest(BaseFeatureTest):
def _check_xss_view_data(self):
print(
"\n\tChecking the SlickGrid cell for XSS vulnerabilities",
"\n\tChecking the Result Grid cell for XSS vulnerabilities",
file=sys.stderr, end=""
)
@ -268,7 +268,7 @@ class CheckForXssFeatureTest(BaseFeatureTest):
self._check_escaped_characters(
source_code,
self.check_xss_chars_set2,
"View Data (SlickGrid)"
"View Data (Result Grid)"
)
def _check_xss_in_explain_module(self):

View File

@ -15,9 +15,10 @@ beforeAll(function () {
jasmine.getEnv().allowRespy(true);
window.addEventListener('error', e => {
console.log(e.message);
if(e.message === 'ResizeObserver loop completed with undelivered notifications.' ||
e.message === 'ResizeObserver loop limit exceeded') {
e.stopImmediatePropagation();
e.stopPropagation();
}
});
});

View File

@ -0,0 +1,111 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import $ from 'jquery';
window.jQuery = window.$ = $;
import 'wcdocker';
import '../helper/enzyme.helper';
import React from 'react';
import { createMount } from '@material-ui/core/test-utils';
import jasmineEnzyme from 'jasmine-enzyme';
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios/index';
// import pgAdmin from 'sources/pgadmin';
import pgWindow from 'sources/window';
import url_for from 'sources/url_for';
import { messages } from '../fake_messages';
import { TreeFake } from '../tree/tree_fake';
import Theme from '../../../pgadmin/static/js/Theme';
import SchemaDiffComponent from '../../../pgadmin/tools/schema_diff/static/js/components/SchemaDiffComponent';
import SchemaDiff from '../../../pgadmin/tools/schema_diff/static/js/SchemaDiffModule';
describe('Schema Diff Component', () => {
let mount;
let mountDOM;
let tree;
let params;
let networkMock;
let schemaDiffInstance;
/* Use createMount so that material ui components gets the required context */
/* https://material-ui.com/guides/testing/#api */
beforeAll(() => {
mount = createMount();
});
beforeEach(() => {
jasmineEnzyme();
// Element for mount wcDocker panel
mountDOM = $('<div class="dockerContainer">');
$(document.body).append(mountDOM);
$(document.body).append($('<div id="debugger-main-container">'));
/* messages used by validators */
pgWindow.pgAdmin.Browser = pgWindow.pgAdmin.Browser || {};
pgWindow.pgAdmin.Browser.messages = pgWindow.pgAdmin.Browser.messages || messages;
pgWindow.pgAdmin.Browser.utils = pgWindow.pgAdmin.Browser.utils || {};
schemaDiffInstance = new SchemaDiff(pgWindow.pgAdmin, pgWindow.pgAdmin.Browser);
// nodeInfo = { parent: {} };
// eslint-disable-next-line
let docker = new wcDocker(
'.dockerContainer', {
allowContextMenu: false,
allowCollapse: false,
loadingClass: 'pg-sp-icon',
});
tree = new TreeFake();
pgWindow.pgAdmin.Browser.tree = tree;
pgWindow.pgAdmin.Browser.docker = docker;
params = {
transId: 1234,
schemaDiff: schemaDiffInstance,
};
networkMock = new MockAdapter(axios);
});
it('SchemaDiff Init', () => {
networkMock.onGet(url_for('schema_diff.servers')).reply(200,
{'success':1,
'errormsg':'',
'info':'',
'result':null,
'data':{
'Servers':[
{'value':1,'label':'PostgreSQL 12','image':'icon-pg','_id':1,'connected':true},
{'value':2,'label':'PostgreSQL 10','image':'icon-server-not-connected','_id':2,'connected':false},
{'value':3,'label':'PostgreSQL 11','image':'icon-server-not-connected','_id':3,'connected':false},
{'value':4,'label':'PostgreSQL 13','image':'icon-server-not-connected','_id':4,'connected':false},
{'value':5,'label':'PostgreSQL 9.6','image':'icon-server-not-connected','_id':5,'connected':false},
{'value':8,'label':'test1234yo','image':'icon-server-not-connected','_id':8,'connected':false}
]
}
}
);
mount(
<Theme>
<SchemaDiffComponent
params={{ transId: params.transId, pgAdmin: pgWindow.pgAdmin }}
>
</SchemaDiffComponent>
</Theme>
);
});
});

View File

@ -1,343 +0,0 @@
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
define([
'sources/selection/active_cell_capture',
'sources/selection/range_selection_helper',
], function (ActiveCellCapture, RangeSelectionHelper) {
describe('ActiveCellCapture', function () {
var grid, activeCellPlugin, getSelectedRangesSpy, setSelectedRangesSpy;
var onHeaderClickFunction;
var onClickFunction;
var onColumnsResizedFunction;
var onHeaderMouseEnterFunction;
var onHeaderMouseLeaveFunction;
var Slick = window.Slick;
beforeEach(function () {
getSelectedRangesSpy = jasmine.createSpy('getSelectedRangesSpy');
setSelectedRangesSpy = jasmine.createSpy('setSelectedRangesSpy');
grid = {
getSelectionModel: function () {
return {
getSelectedRanges: getSelectedRangesSpy,
setSelectedRanges: setSelectedRangesSpy,
};
},
getColumns: function () {
return [
{id: 'row-header-column'},
{id: 'column-1'},
{id: 'column-2'},
];
},
onDragEnd: jasmine.createSpyObj('onDragEnd', ['subscribe']),
onActiveCellChanged: jasmine.createSpyObj('onActiveCellChanged', ['subscribe']),
onHeaderClick: jasmine.createSpy('onHeaderClick'),
onClick: jasmine.createSpy('onClick'),
onKeyDown: jasmine.createSpyObj('onKeyDown', ['subscribe']),
onColumnsResized: jasmine.createSpyObj('onColumnsResized', ['subscribe']),
onHeaderMouseEnter: jasmine.createSpyObj('onHeaderMouseEnter', ['subscribe']),
onHeaderMouseLeave: jasmine.createSpyObj('onHeaderMouseLeave', ['subscribe']),
getDataLength: function () { return 10; },
setActiveCell: jasmine.createSpy('setActiveCell'),
getActiveCell: jasmine.createSpy('getActiveCell'),
resetActiveCell: jasmine.createSpy('resetActiveCell'),
};
activeCellPlugin = new ActiveCellCapture();
grid.onHeaderClick.subscribe =
jasmine.createSpy('subscribe').and.callFake(function (callback) {
onHeaderClickFunction = callback.bind(activeCellPlugin);
});
grid.onClick.subscribe =
jasmine.createSpy('subscribe').and.callFake(function (callback) {
onClickFunction = callback.bind(activeCellPlugin);
});
grid.onColumnsResized.subscribe =
jasmine.createSpy('subscribe').and.callFake(function (callback) {
onColumnsResizedFunction = callback.bind(activeCellPlugin);
});
grid.onHeaderMouseEnter.subscribe =
jasmine.createSpy('subscribe').and.callFake(function (callback) {
onHeaderMouseEnterFunction = callback.bind(activeCellPlugin);
});
grid.onHeaderMouseLeave.subscribe =
jasmine.createSpy('subscribe').and.callFake(function (callback) {
onHeaderMouseLeaveFunction = callback.bind(activeCellPlugin);
});
activeCellPlugin.init(grid);
});
describe('onHeaderClickHandler', function () {
describe('when no ranges are selected', function () {
beforeEach(function () {
getSelectedRangesSpy.and.returnValue([]);
onHeaderClickFunction({}, {column: {pos: 1}});
grid.getActiveCell.and.returnValue(null);
});
it('should set the active cell', function () {
expect(grid.setActiveCell).toHaveBeenCalledWith(0, 2);
});
it('should not change the selected ranges', function () {
expect(setSelectedRangesSpy).not.toHaveBeenCalled();
});
});
describe('when only one column is already selected', function () {
beforeEach(function () {
getSelectedRangesSpy.and.returnValue([RangeSelectionHelper.rangeForColumn(grid, 2)]);
grid.getActiveCell.and.returnValue({cell: 2, row: 0});
});
describe('when a different column is clicked', function () {
beforeEach(function () {
onHeaderClickFunction({}, {column: {pos: 4}});
});
it('should set the active cell to the newly clicked columns top cell', function () {
expect(grid.setActiveCell).toHaveBeenCalledWith(0, 5);
});
});
describe('when the same column is clicked', function () {
beforeEach(function () {
onHeaderClickFunction({}, {column: {pos: 1}});
});
it('should reset the active cell', function () {
expect(grid.resetActiveCell).toHaveBeenCalled();
});
});
describe('when mouse is inside the header row', function () {
beforeEach(function () {
onHeaderMouseEnterFunction({}, {});
});
describe('when user finishes resizing the selected column', function () {
var eventSpy;
beforeEach(function () {
eventSpy = jasmine.createSpyObj('event', ['stopPropagation']);
onColumnsResizedFunction({}, {grid: grid});
onHeaderClickFunction(eventSpy, {column: {pos: 1}});
});
it('should not deselect the current selected column', function () {
expect(grid.setActiveCell).not.toHaveBeenCalled();
expect(grid.resetActiveCell).not.toHaveBeenCalled();
});
it('should prevent further event propagation', function () {
expect(eventSpy.stopPropagation).toHaveBeenCalled();
});
describe('when the user clicks the resized column header', function () {
beforeEach(function () {
eventSpy.stopPropagation.calls.reset();
onHeaderClickFunction(eventSpy, {column: {pos: 2}});
});
it('should change the active cell', function () {
expect(grid.setActiveCell).toHaveBeenCalledWith(0, 3);
expect(grid.resetActiveCell).not.toHaveBeenCalled();
});
it('should allow further event propagation', function () {
expect(eventSpy.stopPropagation).not.toHaveBeenCalled();
});
});
});
});
describe('when mouse is outside the header row', function () {
beforeEach(function () {
onHeaderMouseEnterFunction({}, {});
onHeaderMouseLeaveFunction({}, {});
});
describe('when user finishes resizing the selected column', function () {
beforeEach(function () {
onColumnsResizedFunction({}, {grid: grid});
});
it('should not deselect the current selected column', function () {
expect(grid.setActiveCell).not.toHaveBeenCalled();
expect(grid.resetActiveCell).not.toHaveBeenCalled();
});
describe('when the user clicks another column header', function () {
var eventSpy;
beforeEach(function () {
eventSpy = jasmine.createSpyObj('event', ['stopPropagation']);
onHeaderMouseEnterFunction({}, {});
onHeaderClickFunction(eventSpy, {column: {pos: 3}});
});
it('should change the active cell', function () {
expect(grid.setActiveCell).toHaveBeenCalledWith(0, 4);
expect(grid.resetActiveCell).not.toHaveBeenCalled();
});
it('should allow further event propagation', function () {
expect(eventSpy.stopPropagation).not.toHaveBeenCalled();
});
});
});
});
});
describe('when three non-consecutive columns are selected', function () {
beforeEach(function () {
getSelectedRangesSpy.and.returnValue([
RangeSelectionHelper.rangeForColumn(grid, 10),
RangeSelectionHelper.rangeForColumn(grid, 6),
RangeSelectionHelper.rangeForColumn(grid, 22),
]);
grid.getActiveCell.and.returnValue({cell: 22, row: 0});
});
describe('when the third column is clicked (thereby deselecting it)', function () {
beforeEach(function () {
onHeaderClickFunction({}, {column: {pos: 21}});
});
it('should set the active cell to the second column', function () {
expect(grid.setActiveCell).toHaveBeenCalledWith(0, 6);
});
});
describe('when the second column is clicked (thereby deselecting it)', function () {
beforeEach(function () {
onHeaderClickFunction({}, {column: {pos: 5}});
});
it('should not set the active cell', function () {
expect(grid.setActiveCell).not.toHaveBeenCalled();
});
});
});
});
describe('onClick', function () {
describe('when the target is a random cell in the grid', function () {
it('should not change the active cell', function () {
onClickFunction({}, {row: 1, cell: 2});
grid.getActiveCell.and.returnValue(null);
expect(grid.setActiveCell).not.toHaveBeenCalled();
});
});
describe('when the target is the row header', function () {
describe('when no rows are selected', function () {
beforeEach(function () {
getSelectedRangesSpy.and.returnValue([]);
onClickFunction({}, {row: 1, cell: 0});
grid.getActiveCell.and.returnValue(null);
});
it('changes the active cell', function () {
expect(grid.setActiveCell).toHaveBeenCalledWith(1, 1);
});
it('should not change the selected ranges', function () {
expect(setSelectedRangesSpy).not.toHaveBeenCalled();
});
});
describe('when there is one cell selected', function () {
beforeEach(function () {
grid.getActiveCell.and.returnValue({row: 4, cell: 5});
getSelectedRangesSpy.and.returnValue([
new Slick.Range(4, 5),
]);
});
it('sets the active cell', function () {
onClickFunction({}, {row: 4, cell: 0});
expect(grid.setActiveCell).toHaveBeenCalledWith(4, 1);
});
});
describe('when there is one row selected', function () {
beforeEach(function () {
grid.getActiveCell.and.returnValue({row: 3, cell: 1});
getSelectedRangesSpy.and.returnValue([
RangeSelectionHelper.rangeForRow(grid, 3),
]);
});
it('resets the selected row', function () {
onClickFunction({}, {row: 3, cell: 0});
expect(grid.resetActiveCell).toHaveBeenCalled();
});
describe('when we select a different row', function () {
it('should change the active cell', function () {
onClickFunction({}, {row: 9, cell: 0});
expect(grid.setActiveCell).toHaveBeenCalledWith(9, 1);
});
});
});
describe('when there are 2 rows selected', function () {
beforeEach(function () {
grid.getActiveCell.and.returnValue({row: 3, cell: 1});
getSelectedRangesSpy.and.returnValue([
RangeSelectionHelper.rangeForRow(grid, 5),
RangeSelectionHelper.rangeForRow(grid, 3),
]);
});
describe('when the last selected row is clicked again', function () {
it('should change the active cell to the first selected row', function () {
onClickFunction({}, {row: 3, cell: 0});
expect(grid.setActiveCell).toHaveBeenCalledWith(5, 1);
});
});
describe('when the first selected row is clicked again', function () {
it('should not change the active cell', function () {
onClickFunction({}, {row: 5, cell: 0});
expect(grid.setActiveCell).not.toHaveBeenCalled();
});
});
});
describe('and the editable new row', function () {
beforeEach(function () {
onClickFunction({}, {row: 10, cell: 0});
});
it('does not select the row', function () {
expect(grid.setActiveCell).not.toHaveBeenCalled();
});
});
});
});
});
});

View File

@ -1,414 +0,0 @@
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
import $ from 'jquery';
import Slick from 'slickgrid';
import 'slickgrid.grid';
import ColumnSelector from 'sources/selection/column_selector';
import ActiveCellCapture from 'sources/selection/active_cell_capture';
import 'sources/selection/grid_selector';
import 'slickgrid.plugins/slick.cellrangeselector';
import XCellSelectionModel from 'sources/selection/xcell_selection_model';
describe('ColumnSelector', function () {
var container, data, columns, options;
var SlickGrid = Slick.Grid;
var KEY = {
RIGHT: 39,
LEFT: 37,
UP: 38,
DOWN: 40,
};
beforeEach(function () {
container = $('<div></div>');
container.height(9999);
container.width(9999);
data = [{
'some-column-name': 'first value',
'second column': 'second value',
'third column': 'nonselectable value',
}, {
'some-column-name': 'row 1 - first value',
'second column': 'row 1 - second value',
'third column': 'row 1 - nonselectable value',
}];
columns = [
{
id: 'row-header-column',
name: 'row header column name',
selectable: false,
display_name: 'row header column name',
column_type: 'text',
},
{
id: '1',
name: 'some-column-name',
pos: 0,
display_name: 'some-column-name',
column_type: 'text',
},
{
id: '2',
name: 'second column',
pos: 1,
display_name: 'second column',
column_type: 'json',
},
{
id: 'third-column-id',
name: 'third column',
pos: 2,
display_name: 'third column',
column_type: 'text',
},
{
name: 'some-non-selectable-column',
selectable: false,
pos: 3,
display_name: 'some-non-selectable-column',
column_type: 'numeric',
},
];
});
it('displays the name of the column', function () {
setupGrid(columns);
expect($(container.find('.slick-header-columns .slick-column-name')[1]).text())
.toContain('some-column-name');
expect($(container.find('.slick-header-columns .slick-column-name')[1]).text())
.toContain('text');
expect($(container.find('.slick-header-columns .slick-column-name')[2]).text())
.toContain('second column');
expect($(container.find('.slick-header-columns .slick-column-name')[2]).text())
.toContain('json');
});
it('preserves the other attributes of column definitions', function () {
var columnSelector = new ColumnSelector();
var selectableColumns = columnSelector.getColumnDefinitions(columns);
expect(selectableColumns[1].id).toEqual('1');
});
describe('with ActiveCellCapture, CellSelectionModel, and GridSelector: selecting columns', function () {
var grid, cellSelectionModel;
beforeEach(function () {
var columnSelector = new ColumnSelector();
columns = columnSelector.getColumnDefinitions(columns);
data = [];
for (var i = 0; i < 10; i++) {
data.push({
'some-column-name': 'some-value-' + i,
'second column': 'second value ' + i,
'third column': 'third value ' + i,
'fourth column': 'fourth value ' + i,
});
}
grid = new SlickGrid(container, data, columns);
grid.registerPlugin(new ActiveCellCapture());
cellSelectionModel = new XCellSelectionModel();
grid.setSelectionModel(cellSelectionModel);
grid.registerPlugin(columnSelector);
grid.invalidate();
$('body').append(container);
});
afterEach(function () {
$('body').find(container).remove();
});
let selectColumnAction = ()=> {
container.find('.slick-header-column:contains(some-column-name)').click();
var selectedRanges = cellSelectionModel.getSelectedRanges();
expectOnlyTheFirstColumnToBeSelected(selectedRanges);
};
describe('when the user clicks a column header', function () {
it('selects the column', function () {
selectColumnAction();
});
it('toggles a selected class to the header cell', function () {
container.find('.slick-header-column:contains(second column)').click();
expect($(container.find('.slick-header-column:contains(second column)')).hasClass('selected'))
.toEqual(true);
container.find('.slick-header-column:contains(second column)').click();
expect($(container.find('.slick-header-column:contains(second column)')).hasClass('selected'))
.toEqual(false);
});
});
describe('when the user clicks an additional column header', function () {
beforeEach(function () {
container.find('.slick-header-column:contains(some-column-name)').click();
container.find('.slick-header-column:contains(second column)').click();
});
it('selects additional columns', function () {
var selectedRanges = cellSelectionModel.getSelectedRanges();
expect(selectedRanges.length).toEqual(2);
var column1 = selectedRanges[0];
expect(column1.fromCell).toEqual(1);
expect(column1.toCell).toEqual(1);
var column2 = selectedRanges[1];
expect(column2.fromCell).toEqual(2);
expect(column2.toCell).toEqual(2);
});
describe('and presses shift + right-arrow', function () {
beforeEach(function () {
pressShiftArrow(KEY.RIGHT);
});
it('keeps the last column selected', function () {
expect(cellSelectionModel.getSelectedRanges().length).toEqual(1);
});
it('grows the selection to the right', function () {
var selectedRange = cellSelectionModel.getSelectedRanges()[0];
expect(selectedRange.fromCell).toEqual(2);
expect(selectedRange.toCell).toEqual(3);
expect(selectedRange.fromRow).toEqual(0);
expect(selectedRange.toRow).toEqual(9);
});
it('keeps selected class on columns 2 and 3', function () {
expect($(container.find('.slick-header-column:contains(second column)')).hasClass('selected'))
.toEqual(true);
expect($(container.find('.slick-header-column:contains(third column)')).hasClass('selected'))
.toEqual(true);
expect($(container.find('.slick-header-column:contains(some-column-name)')).hasClass('selected'))
.toEqual(false);
});
});
describe('when the user deselects the last selected column header', function () {
beforeEach(function () {
container.find('.slick-header-column:contains(second column)').click();
});
describe('and presses shift + right-arrow', function () {
it('first and second columns are selected', function () {
pressShiftArrow(KEY.RIGHT);
var selectedRanges = cellSelectionModel.getSelectedRanges();
expect(selectedRanges.length).toEqual(1);
expect(selectedRanges[0].fromCell).toEqual(1);
expect(selectedRanges[0].toCell).toEqual(2);
expect(selectedRanges[0].fromRow).toEqual(0);
expect(selectedRanges[0].toRow).toEqual(9);
});
});
});
});
describe('when the user clicks a column header description', function () {
it('selects the column', function () {
container.find('.slick-header-columns span.column-description:contains(some-column-name)').click();
var selectedRanges = cellSelectionModel.getSelectedRanges();
expectOnlyTheFirstColumnToBeSelected(selectedRanges);
});
it('toggles a selected class to the header cell', function () {
container.find('.slick-header-column span.column-description:contains(second column)').click();
expect($(container.find('.slick-header-column:contains(second column)')).hasClass('selected'))
.toEqual(true);
container.find('.slick-header-column span.column-description:contains(second column)').click();
expect($(container.find('.slick-header-column:contains(second column)')).hasClass('selected'))
.toEqual(false);
});
});
describe('when a row is selected', function () {
beforeEach(function () {
var selectedRanges = [new Slick.Range(0, 0, 0, 1)];
cellSelectionModel.setSelectedRanges(selectedRanges);
});
it('deselects the row', function () {
container.find('.slick-header-column')[1].click();
var selectedRanges = cellSelectionModel.getSelectedRanges();
expect(selectedRanges.length).toEqual(1);
var column = selectedRanges[0];
expect(column.fromCell).toEqual(1);
expect(column.toCell).toEqual(1);
expect(column.fromRow).toEqual(0);
expect(column.toRow).toEqual(9);
});
});
describe('clicking a second time', function () {
beforeEach(function () {
container.find('.slick-header-column')[1].click();
});
it('deselects the column', function () {
container.find('.slick-header-column')[1].click();
var selectedRanges = cellSelectionModel.getSelectedRanges();
expect(selectedRanges.length).toEqual(0);
});
});
describe('when the column is not selectable', function () {
it('does not select the column', function () {
$(container.find('.slick-header-column:contains(some-non-selectable-column)')).trigger('click');
var selectedRanges = cellSelectionModel.getSelectedRanges();
expect(selectedRanges.length).toEqual(0);
});
});
describe('when a non-column range was already selected', function () {
beforeEach(function () {
var selectedRanges = [new Slick.Range(0, 0, 2, 0)];
cellSelectionModel.setSelectedRanges(selectedRanges);
});
it('deselects the non-column range', function () {
selectColumnAction();
});
});
describe('when a column is selected', function () {
beforeEach(function () {
container.find('.slick-header-column:contains(some-column-name)').click();
});
describe('when the user click a cell on the current range', function () {
beforeEach(function () {
container.find('.slick-cell.l1.r1')[1].click();
});
it('column is deselected', function () {
var selectedRanges = cellSelectionModel.getSelectedRanges();
expect(selectedRanges.length).toEqual(1);
var column = selectedRanges[0];
expect(column.fromCell).toEqual(1);
expect(column.toCell).toEqual(1);
expect(column.fromRow).toEqual(1);
expect(column.toRow).toEqual(1);
});
it('keep select class on column header', function () {
expect($(container.find('.slick-header-column:contains(some-column-name)')).hasClass('selected'))
.toBeTruthy();
});
});
describe('when the user click a cell outside the current range', function () {
beforeEach(function () {
container.find('.slick-cell.l2.r2')[2].click();
});
it('column is deselected', function () {
var selectedRanges = cellSelectionModel.getSelectedRanges();
expect(selectedRanges.length).toEqual(1);
var column = selectedRanges[0];
expect(column.fromCell).toEqual(2);
expect(column.toCell).toEqual(2);
expect(column.fromRow).toEqual(2);
expect(column.toRow).toEqual(2);
});
it('remove select class on \'some-column-name\' column header', function () {
expect($(container.find('.slick-header-column:contains(some-column-name)')).hasClass('selected'))
.toBeFalsy();
expect($(container.find('.slick-header-column:contains(second column)')).hasClass('selected'))
.toBeTruthy();
});
});
describe('when the user click in a row header', function () {
beforeEach(function () {
var selectedRanges = [new Slick.Range(1, 1, 1, 3)];
cellSelectionModel.setSelectedRanges(selectedRanges);
});
it('column is deselected', function () {
var selectedRanges = cellSelectionModel.getSelectedRanges();
expect(selectedRanges.length).toEqual(1);
var column = selectedRanges[0];
expect(column.fromCell).toEqual(1);
expect(column.toCell).toEqual(3);
expect(column.fromRow).toEqual(1);
expect(column.toRow).toEqual(1);
});
it('no column should have the class \'selected\'', function () {
expect($(container.find('.slick-header-column:contains(some-column-name)')).hasClass('selected'))
.toBeTruthy();
});
});
});
});
function setupGrid(gridColumns) {
var columnSelector = new ColumnSelector();
gridColumns = columnSelector.getColumnDefinitions(gridColumns);
var grid = new SlickGrid(container, data, gridColumns, options);
var cellSelectionModel = new XCellSelectionModel();
grid.setSelectionModel(cellSelectionModel);
grid.registerPlugin(columnSelector);
grid.invalidate();
}
function expectOnlyTheFirstColumnToBeSelected(selectedRanges) {
var row = selectedRanges[0];
expect(selectedRanges.length).toEqual(1);
expect(row.fromCell).toEqual(1);
expect(row.toCell).toEqual(1);
expect(row.fromRow).toEqual(0);
expect(row.toRow).toEqual(9);
}
function pressShiftArrow(keyCode) {
var pressEvent = new $.Event('keydown');
pressEvent.shiftKey = true;
pressEvent.ctrlKey = false;
pressEvent.altKey = false;
pressEvent.which = keyCode;
$(container.find('.grid-canvas')).trigger(pressEvent);
}
});

View File

@ -1,150 +0,0 @@
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
import $ from 'jquery';
import Slick from 'slickgrid';
import 'slickgrid.grid';
import clipboard from '../../../pgadmin/static/js/selection/clipboard';
import copyData from '../../../pgadmin/static/js/selection/copy_data';
import RangeSelectionHelper from 'sources/selection/range_selection_helper';
import XCellSelectionModel from 'sources/selection/xcell_selection_model';
describe('copyData', function () {
var grid, sqlEditor, gridContainer, buttonPasteRow, buttonCopyWithHeader;
var SlickGrid;
beforeEach(function () {
SlickGrid = Slick.Grid;
var data = [{'id': 1, 'brand':'leopord', 'size':12, '__temp_PK': '123'},
{'id': 2, 'brand':'lion', 'size':13, '__temp_PK': '456'},
{'id': 3, 'brand':'puma', 'size':9, '__temp_PK': '789'}],
dataView = new Slick.Data.DataView();
var CSVOptions = {'quoting': 'strings', 'quote_char': '"', 'field_separator': ','};
var columns = [
{
id: 'row-header-column',
name: 'row header column name',
selectable: false,
display_name: 'row header column name',
column_type: 'text',
},
{
name: 'id',
field: 'id',
pos: 0,
label: 'id<br> numeric',
cell: 'number',
can_edit: false,
type: 'numeric',
}, {
name: 'brand',
field: 'brand',
pos: 1,
label: 'flavor<br> character varying',
cell: 'string',
can_edit: false,
type: 'character varying',
}, {
name: 'size',
field: 'size',
pos: 2,
label: 'size<br> numeric',
cell: 'number',
can_edit: false,
type: 'numeric',
},
];
gridContainer = $('<div id="grid"></div>');
$('body').append(gridContainer);
buttonPasteRow = $('<button id="btn-paste-row" disabled></button>');
buttonCopyWithHeader = $('<button class="copy-with-header visibility-hidden"></button>');
$('body').append(buttonPasteRow);
$('body').append(buttonCopyWithHeader);
grid = new SlickGrid('#grid', dataView, columns, {});
grid.CSVOptions = CSVOptions;
dataView.setItems(data, '__temp_PK');
grid.setSelectionModel(new XCellSelectionModel());
sqlEditor = {slickgrid: grid};
});
afterEach(function() {
grid.destroy();
gridContainer.remove();
buttonPasteRow.remove();
buttonCopyWithHeader.remove();
});
describe('when rows are selected', function () {
beforeEach(function () {
grid.getSelectionModel().setSelectedRanges([
RangeSelectionHelper.rangeForRow(grid, 0),
RangeSelectionHelper.rangeForRow(grid, 2)]
);
});
it('copies them', function () {
spyOn(clipboard, 'copyTextToClipboard').and.callThrough();
copyData.apply(sqlEditor);
expect(sqlEditor.copied_rows.length).toEqual(2);
expect(clipboard.copyTextToClipboard).toHaveBeenCalled();
expect(clipboard.copyTextToClipboard.calls.mostRecent().args[0]).toContain('1,"leopord",12');
expect(clipboard.copyTextToClipboard.calls.mostRecent().args[0]).toContain('3,"puma",9');
});
describe('when the user can edit the grid', function () {
it('enables the paste row button', function () {
copyData.apply(_.extend({can_edit: true}, sqlEditor));
expect($('#btn-paste-row').prop('disabled')).toEqual(false);
});
});
});
describe('when a column is selected', function () {
beforeEach(function () {
var firstDataColumn = RangeSelectionHelper.rangeForColumn(grid, 1);
grid.getSelectionModel().setSelectedRanges([firstDataColumn]);
});
it('copies text to the clipboard', function () {
spyOn(clipboard, 'copyTextToClipboard');
copyData.apply(sqlEditor);
expect(clipboard.copyTextToClipboard).toHaveBeenCalled();
var copyArg = clipboard.copyTextToClipboard.calls.mostRecent().args[0];
var rowStrings = copyArg.split('\n');
expect(rowStrings[0]).toEqual('1');
expect(rowStrings[1]).toEqual('2');
expect(rowStrings[2]).toEqual('3');
});
it('sets copied_rows to empty', function () {
copyData.apply(sqlEditor);
expect(sqlEditor.copied_rows.length).toEqual(0);
});
describe('when the user can edit the grid', function () {
beforeEach(function () {
copyData.apply(_.extend({can_edit: true}, sqlEditor));
});
it('disables the paste row button', function () {
expect($('#btn-paste-row').prop('disabled')).toEqual(true);
});
});
});
});

View File

@ -1,128 +0,0 @@
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
import $ from 'jquery';
import Slick from 'slickgrid';
import 'slickgrid.grid';
import GridSelector from 'sources/selection/grid_selector';
import XCellSelectionModel from 'sources/selection/xcell_selection_model';
describe('GridSelector', function () {
var container, data, columns, gridSelector, xCellSelectionModel;
var SlickGrid;
beforeEach(function () {
SlickGrid = Slick.Grid;
container = $('<div></div>');
container.height(9999);
columns = [{
id: '1',
name: 'some-column-name',
pos: 0,
}, {
id: '2',
name: 'second column',
pos: 1,
}];
gridSelector = new GridSelector();
columns = gridSelector.getColumnDefinitions(columns);
data = [];
for (var i = 0; i < 10; i++) {
data.push({'some-column-name': 'some-value-' + i, 'second column': 'second value ' + i});
}
var grid = new SlickGrid(container, data, columns);
xCellSelectionModel = new XCellSelectionModel();
grid.setSelectionModel(xCellSelectionModel);
grid.registerPlugin(gridSelector);
grid.invalidate();
$('body').append(container);
});
afterEach(function () {
$('body').find(container).remove();
});
it('renders an additional column on the left for selecting rows', function () {
expect(columns.length).toEqual(3);
var leftmostColumn = columns[0];
expect(leftmostColumn.id).toEqual('row-header-column');
});
it('renders a button for selecting all the cells', function () {
expect(container.find('[title=\'Select/Deselect All\']').length).toEqual(1);
});
describe('when the cell for the select/deselect all is clicked', function () {
it('selects the whole grid', function () {
container.find('[title=\'Select/Deselect All\']').parent().trigger('click');
var selectedRanges = xCellSelectionModel.getSelectedRanges();
expect(selectedRanges.length).toEqual(1);
var selectedRange = selectedRanges[0];
expect(selectedRange.fromCell).toEqual(1);
expect(selectedRange.toCell).toEqual(2);
expect(selectedRange.fromRow).toEqual(0);
expect(selectedRange.toRow).toEqual(9);
});
it('adds selected class', function () {
container.find('[title=\'Select/Deselect All\']').parent().trigger('click');
expect($(container.find('[data-id=\'select-all\']')).hasClass('selected')).toBeTruthy();
});
});
describe('when the select all button in the corner gets selected', function () {
it('selects all the cells', function () {
container.find('[title=\'Select/Deselect All\']').trigger('click');
var selectedRanges = xCellSelectionModel.getSelectedRanges();
expect(selectedRanges.length).toEqual(1);
var selectedRange = selectedRanges[0];
expect(selectedRange.fromCell).toEqual(1);
expect(selectedRange.toCell).toEqual(2);
expect(selectedRange.fromRow).toEqual(0);
expect(selectedRange.toRow).toEqual(9);
});
describe('when the select all button in the corner gets deselected', function () {
beforeEach(function () {
container.find('[title=\'Select/Deselect All\']').trigger('click');
});
it('deselects all the cells', function () {
container.find('[title=\'Select/Deselect All\']').trigger('click');
var selectedRanges = xCellSelectionModel.getSelectedRanges();
expect(selectedRanges.length).toEqual(0);
});
});
describe('and then the underlying selection changes', function () {
beforeEach(function () {
container.find('[title=\'Select/Deselect All\']').trigger('click');
});
it('removes the selected class', function () {
container.find('[title=\'Select/Deselect All\']').parent().trigger('click');
expect($(container.find('[data-id=\'select-all\']')).hasClass('selected')).toBeFalsy();
});
});
});
});

View File

@ -1,189 +0,0 @@
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
import rangeBoundaryNavigator from 'sources/selection/range_boundary_navigator';
import Slick from 'slickgrid';
describe('RangeBoundaryNavigator', function () {
describe('#getUnion', function () {
describe('when the ranges completely overlap', function () {
it('returns a list with that range', function () {
var ranges = [[1, 4], [1, 4], [1, 4]];
var union = rangeBoundaryNavigator.getUnion(ranges);
expect(union).toEqual([[1, 4]]);
});
});
describe('when the ranges all overlap partially or touch', function () {
it('returns one long range', function () {
var rangeBounds = [[3, 6], [1, 4], [7, 14]];
var union = rangeBoundaryNavigator.getUnion(rangeBounds);
expect(union).toEqual([[1, 14]]);
});
it('returns them in order from lowest to highest', function () {
var rangeBounds = [[3, 6], [2, 3], [10, 12]];
var union = rangeBoundaryNavigator.getUnion(rangeBounds);
expect(union).toEqual([[2, 6], [10, 12]]);
});
describe('when one range completely overlaps another', function() {
it('returns them in order from lowest to highest', function () {
var rangeBounds = [[9, 14], [2, 3], [11, 13]];
var union = rangeBoundaryNavigator.getUnion(rangeBounds);
expect(union).toEqual([[2, 3], [9, 14]]);
});
});
describe('when one range is a subset of another', function () {
it('returns the larger range', function () {
var rangeBounds = [[2, 6], [1, 14], [8, 10]];
var union = rangeBoundaryNavigator.getUnion(rangeBounds);
expect(union).toEqual([[1, 14]]);
});
});
});
describe('when the ranges do not touch', function () {
it('returns them in order from lowest to highest', function () {
var rangeBounds = [[3, 6], [1, 1], [8, 10]];
var union = rangeBoundaryNavigator.getUnion(rangeBounds);
expect(union).toEqual([[1, 1], [3, 6], [8, 10]]);
});
});
});
describe('#mapDimensionBoundaryUnion', function () {
it('returns a list of the results of the callback', function () {
var rangeBounds = [[0, 1], [3, 3]];
var callback = function () {
return 'hello';
};
var result = rangeBoundaryNavigator.mapDimensionBoundaryUnion(rangeBounds, callback);
expect(result).toEqual(['hello', 'hello', 'hello']);
});
it('calls the callback with each index in the dimension', function () {
var rangeBounds = [[0, 1], [3, 3]];
var callback = jasmine.createSpy('callbackSpy');
rangeBoundaryNavigator.mapDimensionBoundaryUnion(rangeBounds, callback);
expect(callback.calls.allArgs()).toEqual([[0], [1], [3]]);
});
});
describe('#mapOver2DArray', function () {
var data, rowCollector, processCell;
beforeEach(function () {
data = [[0, 1, 2, 3], [2, 2, 2, 2], [4, 5, 6, 7]];
processCell = function (rowIndex, columnIndex) {
return data[rowIndex][columnIndex];
};
rowCollector = function (rowData) {
return JSON.stringify(rowData);
};
});
it('calls the callback for each item in the ranges', function () {
var rowRanges = [[0, 0], [2, 2]];
var colRanges = [[0, 3]];
var selectionResult = rangeBoundaryNavigator.mapOver2DArray(rowRanges, colRanges, processCell, rowCollector);
expect(selectionResult).toEqual(['[0,1,2,3]', '[4,5,6,7]']);
});
describe('when the ranges are out of order/duplicated', function () {
var rowRanges, colRanges;
beforeEach(function () {
rowRanges = [[2, 2], [2, 2], [0, 0]];
colRanges = [[0, 3]];
});
it('uses the union of the ranges', function () {
spyOn(rangeBoundaryNavigator, 'getUnion').and.callThrough();
var selectionResult = rangeBoundaryNavigator.mapOver2DArray(rowRanges, colRanges, processCell, rowCollector);
expect(rangeBoundaryNavigator.getUnion).toHaveBeenCalledWith(rowRanges);
expect(rangeBoundaryNavigator.getUnion).toHaveBeenCalledWith(colRanges);
expect(selectionResult).toEqual(['[0,1,2,3]', '[4,5,6,7]']);
});
});
});
describe('#rangesToCsv', function () {
var data, columnDefinitions, ranges, CSVOptions;
beforeEach(function () {
data = [{'id':1, 'animal':'leopard', 'size':'12'},
{'id':2, 'animal':'lion', 'size':'13'},
{'id':3, 'animal':'cougar', 'size':'9'},
{'id':4, 'animal':'tiger', 'size':'10'}];
columnDefinitions = [{name: 'id', field: 'id', pos: 0, cell:'number'},
{name: 'animal', field: 'animal', pos: 1, cell:'string'},
{name: 'size', field: 'size', pos: 2, cell:'string'}];
ranges = [new Slick.Range(0, 0, 0, 2), new Slick.Range(3, 0, 3, 2)];
CSVOptions = [{'quoting': 'all', 'quote_char': '"', 'field_separator': ','},
{'quoting': 'strings', 'quote_char': '"', 'field_separator': ';'},
{'quoting': 'strings', 'quote_char': '\'', 'field_separator': '|'},
{'quoting': 'none', 'quote_char': '"', 'field_separator': '\t'}];
});
it('returns csv for the provided ranges for CSV options quoting All with char " with field separator ,', function () {
var csvResult = rangeBoundaryNavigator.rangesToCsv(data, columnDefinitions, ranges, CSVOptions[0]);
expect(csvResult).toEqual('"1","leopard","12"\n"4","tiger","10"');
});
describe('when no cells are selected for CSV options quoting Strings with char " with field separator ;', function () {
it('should return an empty string', function () {
var csvResult = rangeBoundaryNavigator.rangesToCsv(data, columnDefinitions, [], CSVOptions[1]);
expect(csvResult).toEqual('');
});
});
describe('when there is an extra column with checkboxes', function () {
beforeEach(function () {
columnDefinitions = [{name: 'not-a-data-column'},
{name: 'id', field: 'id', pos: 0, cell:'number'},
{name: 'animal', field: 'animal', pos: 1, cell:'string'},
{name: 'size', field: 'size',pos: 2, cell:'string'}];
ranges = [new Slick.Range(0, 0, 0, 3), new Slick.Range(3, 0, 3, 3)];
});
it('returns csv for the columns with data for CSV options quoting Strings with char \' with field separator |', function () {
var csvResult = rangeBoundaryNavigator.rangesToCsv(data, columnDefinitions, ranges, CSVOptions[2]);
expect(csvResult).toEqual('1|\'leopard\'|\'12\'\n4|\'tiger\'|\'10\'');
});
describe('when no cells are selected for CSV options quoting none with field separator tab', function () {
it('should return an empty string', function () {
var csvResult = rangeBoundaryNavigator.rangesToCsv(data, columnDefinitions, [], CSVOptions[3]);
expect(csvResult).toEqual('');
});
});
});
});
});

View File

@ -1,100 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import $ from 'jquery';
import Slick from 'slickgrid';
import 'slickgrid.grid';
import RangeSelectionHelper from 'sources/selection/range_selection_helper';
describe('RangeSelectionHelper utility functions', function () {
var grid;
beforeEach(function () {
var container, data, columns, options;
container = $('<div></div>');
container.height(9999);
columns = [{
id: '1',
name: 'some-column-name',
pos: 0,
}, {
id: 'second-column-id',
name: 'second column',
pos: 1,
}];
data = [];
for (var i = 0; i < 10; i++) {
data.push({'some-column-name': 'some-value-' + i, 'second column': 'second value ' + i});
}
grid = new Slick.Grid(container, data, columns, options);
grid.invalidate();
});
describe('#getIndexesOfCompleteRows', function () {
describe('when selected ranges are not rows', function () {
it('returns an empty array', function () {
var rowlessRanges = [RangeSelectionHelper.rangeForColumn(grid, 1)];
expect(RangeSelectionHelper.getIndexesOfCompleteRows(grid, rowlessRanges))
.toEqual([]);
});
});
describe('when selected range', function () {
describe('is a single row', function () {
it('returns an array with one index', function () {
var singleRowRange = [RangeSelectionHelper.rangeForRow(grid, 1)];
expect(RangeSelectionHelper.getIndexesOfCompleteRows(grid, singleRowRange))
.toEqual([1]);
});
});
describe('is multiple rows', function () {
it('returns an array of each row\'s index', function () {
var multipleRowRange = [
RangeSelectionHelper.rangeForRow(grid, 0),
RangeSelectionHelper.rangeForRow(grid, 3),
RangeSelectionHelper.rangeForRow(grid, 2),
];
var indexesOfCompleteRows = RangeSelectionHelper.getIndexesOfCompleteRows(grid, multipleRowRange);
indexesOfCompleteRows.sort();
expect(indexesOfCompleteRows).toEqual([0, 2, 3]);
});
});
describe('contains a multi row selection', function () {
it('returns an array of each individual row\'s index', function () {
var multipleRowRange = [
new Slick.Range(3, 0, 5, 1),
];
var indexesOfCompleteRows = RangeSelectionHelper.getIndexesOfCompleteRows(grid, multipleRowRange);
indexesOfCompleteRows.sort();
expect(indexesOfCompleteRows).toEqual([3, 4, 5]);
});
describe('and also contains a selection that is not a row', function () {
it('returns an array of only the complete rows\' indexes', function () {
var multipleRowRange = [
new Slick.Range(8, 1, 9, 1),
new Slick.Range(3, 0, 5, 1),
];
var indexesOfCompleteRows = RangeSelectionHelper.getIndexesOfCompleteRows(grid, multipleRowRange);
indexesOfCompleteRows.sort();
expect(indexesOfCompleteRows).toEqual([3, 4, 5]);
});
});
});
});
});
});

View File

@ -1,328 +0,0 @@
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
import $ from 'jquery';
import Slick from 'slickgrid';
import 'slickgrid.grid';
import RowSelector from 'sources/selection/row_selector';
import ActiveCellCapture from 'sources/selection/active_cell_capture';
import XCellSelectionModel from 'sources/selection/xcell_selection_model';
describe('RowSelector', function () {
var KEY = {
RIGHT: 39,
LEFT: 37,
UP: 38,
DOWN: 40,
};
var container, dataView, columnDefinitions, grid, cellSelectionModel;
var SlickGrid = Slick.Grid;
beforeEach(function () {
container = $('<div></div>');
container.height(9999);
container.width(9999);
columnDefinitions = [{
id: '1',
name: 'some-column-name',
selectable: true,
pos: 0,
}, {
id: '2',
name: 'second column',
selectable: true,
pos: 1,
}];
dataView = new Slick.Data.DataView();
var rowSelector = new RowSelector();
var data = [];
for (var i = 0; i < 10; i++) {
data.push({'some-column-name':'some-value-' + i, 'second column':'second value ' + i});
}
columnDefinitions = rowSelector.getColumnDefinitions(columnDefinitions);
dataView.setItems(data, 'some-column-name');
grid = new SlickGrid(container, dataView, columnDefinitions);
grid.registerPlugin(new ActiveCellCapture());
cellSelectionModel = new XCellSelectionModel();
grid.setSelectionModel(cellSelectionModel);
grid.registerPlugin(rowSelector);
grid.invalidate();
$('body').append(container);
});
afterEach(function () {
$('body').find(container).remove();
});
it('renders an additional column on the left', function () {
expect(columnDefinitions.length).toEqual(3);
var leftmostColumn = columnDefinitions[0];
expect(leftmostColumn.id).toEqual('row-header-column');
expect(leftmostColumn.name).toEqual('');
expect(leftmostColumn.selectable).toEqual(false);
});
it('renders a span on the leftmost column', function () {
expect(container.find('.slick-row').length).toEqual(10);
expect(container.find('.slick-row .slick-cell:first-child span[data-cell-type="row-header-selector"]').length).toEqual(10);
});
it('preserves the other attributes of column definitions', function () {
expect(columnDefinitions[1].id).toEqual('1');
expect(columnDefinitions[1].selectable).toEqual(true);
});
describe('selecting rows', function () {
describe('when the user clicks a row header span', function () {
it('selects the row', function () {
container.find('.slick-row .slick-cell:first-child span[data-cell-type="row-header-selector"]')[0].click();
var selectedRanges = cellSelectionModel.getSelectedRanges();
expectOnlyTheFirstRowToBeSelected(selectedRanges);
});
it('add selected class to parent of the span', function () {
container.find('.slick-row .slick-cell:first-child span[data-cell-type="row-header-selector"]')[5].click();
expect($(container.find('.slick-row .slick-cell:first-child ')[5])
.hasClass('selected')).toBeTruthy();
});
});
describe('when the user clicks a row header', function () {
beforeEach(function () {
container.find('.slick-row .slick-cell:first-child')[1].click();
});
it('selects the row', function () {
var selectedRanges = cellSelectionModel.getSelectedRanges();
var row = selectedRanges[0];
expect(selectedRanges.length).toEqual(1);
expect(row.fromCell).toEqual(1);
expect(row.toCell).toEqual(2);
expect(row.fromRow).toEqual(1);
expect(row.toRow).toEqual(1);
});
it('add selected class to parent of the span', function () {
expect($(container.find('.slick-row .slick-cell:first-child ')[1])
.hasClass('selected')).toBeTruthy();
});
describe('when the user clicks again the same row header', function () {
it('add selected class to parent of the span', function () {
container.find('.slick-row .slick-cell:first-child span[data-cell-type="row-header-selector"]')[1].click();
expect($(container.find('.slick-row .slick-cell:first-child ')[1])
.hasClass('selected')).toBeFalsy();
});
});
describe('and presses shift + down-arrow', function () {
beforeEach(function () {
pressShiftArrow(KEY.DOWN);
});
it('keeps the last row selected', function () {
expect(cellSelectionModel.getSelectedRanges().length).toEqual(1);
});
it('grows the selection down', function () {
var selectedRanges = cellSelectionModel.getSelectedRanges();
var row = selectedRanges[0];
expect(selectedRanges.length).toEqual(1);
expect(row.fromCell).toEqual(1);
expect(row.toCell).toEqual(2);
expect(row.fromRow).toEqual(1);
expect(row.toRow).toEqual(2);
});
it('keeps selected class on rows 1 and 2', function () {
expect($(container.find('.slick-row .slick-cell:first-child ')[0])
.hasClass('selected')).toBeFalsy();
expect($(container.find('.slick-row .slick-cell:first-child ')[1])
.hasClass('selected')).toBeTruthy();
expect($(container.find('.slick-row .slick-cell:first-child ')[2])
.hasClass('selected')).toBeTruthy();
expect($(container.find('.slick-row .slick-cell:first-child ')[3])
.hasClass('selected')).toBeFalsy();
});
});
describe('when the user clicks a cell on the current range', function () {
beforeEach(function () {
container.find('.slick-cell.l1.r1')[5].click();
});
it('row gets deselected', function () {
var selectedRanges = cellSelectionModel.getSelectedRanges();
expect(selectedRanges.length).toEqual(1);
var newSelection = selectedRanges[0];
expect(newSelection.fromCell).toEqual(1);
expect(newSelection.fromRow).toEqual(5);
expect(newSelection.toCell).toEqual(1);
expect(newSelection.toRow).toEqual(5);
});
it('keep select class on row header', function () {
expect($(container.find('.slick-cell.l0.r0')[5]).hasClass('selected'))
.toBeTruthy();
});
});
describe('when the user clicks a cell outside the current range', function () {
beforeEach(function () {
container.find('.slick-cell.l2.r2')[2].click();
});
it('row gets deselected', function () {
var selectedRanges = cellSelectionModel.getSelectedRanges();
expect(selectedRanges.length).toEqual(1);
var newSelection = selectedRanges[0];
expect(newSelection.fromCell).toEqual(2);
expect(newSelection.fromRow).toEqual(2);
expect(newSelection.toCell).toEqual(2);
expect(newSelection.toRow).toEqual(2);
});
it('remove select class on "some-column-name" column header', function () {
expect($(container.find('.slick-cell.l0.r0')[5]).hasClass('selected'))
.toBeFalsy();
expect($(container.find('.slick-cell.l0.r0')[2]).hasClass('selected'))
.toBeTruthy();
});
});
describe('when the user has a column selected', function () {
beforeEach(function () {
var selectedRanges = [new Slick.Range(0, 1, 9, 1)];
cellSelectionModel.setSelectedRanges(selectedRanges);
});
it('no row should have the class "selected"', function () {
expect($(container.find('.slick-cell.l0.r0')[0]).hasClass('selected'))
.toBeFalsy();
expect($(container.find('.slick-cell.l0.r0')[1]).hasClass('selected'))
.toBeFalsy();
expect($(container.find('.slick-cell.l0.r0')[2]).hasClass('selected'))
.toBeFalsy();
expect($(container.find('.slick-cell.l0.r0')[3]).hasClass('selected'))
.toBeFalsy();
expect($(container.find('.slick-cell.l0.r0')[4]).hasClass('selected'))
.toBeFalsy();
expect($(container.find('.slick-cell.l0.r0')[5]).hasClass('selected'))
.toBeFalsy();
});
});
});
describe('when the user clicks multiple row headers', function () {
it('selects another row', function () {
container.find('.slick-row .slick-cell:first-child')[4].click();
container.find('.slick-row .slick-cell:first-child')[0].click();
var selectedRanges = cellSelectionModel.getSelectedRanges();
expect(selectedRanges.length).toEqual(2);
var row1 = selectedRanges[0];
expect(row1.fromRow).toEqual(4);
expect(row1.toRow).toEqual(4);
var row2 = selectedRanges[1];
expect(row2.fromRow).toEqual(0);
expect(row2.toRow).toEqual(0);
});
});
describe('when a column was already selected', function () {
beforeEach(function () {
var selectedRanges = [new Slick.Range(0, 0, 0, 1)];
cellSelectionModel.setSelectedRanges(selectedRanges);
});
it('deselects the column', function () {
container.find('.slick-row .slick-cell:first-child')[0].click();
var selectedRanges = cellSelectionModel.getSelectedRanges();
expectOnlyTheFirstRowToBeSelected(selectedRanges);
});
});
describe('when the row is deselected through setSelectedRanges', function () {
beforeEach(function () {
container.find('.slick-row .slick-cell:first-child')[4].click();
});
it('should remove the selected class', function () {
cellSelectionModel.setSelectedRanges([]);
expect($(container.find('.slick-row .slick-cell:first-child span[data-cell-type="row-header-selector"]')[4])
.hasClass('selected')).toBeFalsy();
});
});
describe('click a second time', function () {
beforeEach(function () {
container.find('.slick-row .slick-cell:first-child')[1].click();
});
it('removes the selected class', function () {
container.find('.slick-row .slick-cell:first-child')[1].click();
expect($(container.find('.slick-row .slick-cell:first-child span[data-cell-type="row-header-selector"]')[1])
.hasClass('selected')).toBeFalsy();
});
it('unselects the row', function () {
container.find('.slick-row .slick-cell:first-child')[1].click();
var selectedRanges = cellSelectionModel.getSelectedRanges();
expect(selectedRanges.length).toEqual(0);
});
});
});
function pressShiftArrow(keyCode) {
var pressEvent = new $.Event('keydown');
pressEvent.shiftKey = true;
pressEvent.ctrlKey = false;
pressEvent.altKey = false;
pressEvent.which = keyCode;
$(container.find('.grid-canvas')).trigger(pressEvent);
}
function expectOnlyTheFirstRowToBeSelected(selectedRanges) {
var row = selectedRanges[0];
expect(selectedRanges.length).toEqual(1);
expect(row.fromCell).toEqual(1);
expect(row.toCell).toEqual(2);
expect(row.fromRow).toEqual(0);
expect(row.toRow).toEqual(0);
}
});

View File

@ -1,253 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import $ from 'jquery';
import 'slickgrid.grid';
import Slick from 'slickgrid';
import SetStagedRows from 'sources/selection/set_staged_rows';
describe('set_staged_rows', function () {
var sqlEditorObj, gridSpy, deleteButton, copyButton, selectionSpy;
beforeEach(function () {
var data = [{'a pk column': 'one', 'some column': 'two', '__temp_PK': '123'},
{'a pk column': 'three', 'some column': 'four', '__temp_PK': '456'},
{'a pk column': 'five', 'some column': 'six', '__temp_PK': '789'},
{'a pk column': 'seven', 'some column': 'eight', '__temp_PK': '432'}],
dataView = new Slick.Data.DataView();
dataView.setItems(data, '__temp_PK');
gridSpy = jasmine.createSpyObj('gridSpy', ['getData', 'getCellNode', 'getColumns']);
gridSpy.getData.and.returnValue(dataView);
gridSpy.getColumns.and.returnValue([
{
name: 'a pk column',
field: 'a pk column',
pos: 0,
selectable: true,
}, {
name: 'some column',
field: 'some column',
pos: 1,
selectable: true,
},
]);
selectionSpy = jasmine.createSpyObj('selectionSpy', ['setSelectedRows', 'getSelectedRanges']);
deleteButton = $('<button id="btn-delete-row"></button>');
copyButton = $('<button id="btn-copy-row"></button>');
sqlEditorObj = {
grid: gridSpy,
editor: {
handler: {
data_store: {
staged_rows: {'456': {}},
},
can_edit: false,
},
},
keys: null,
selection: selectionSpy,
columns: [
{
name: 'a pk column',
field: 'a pk column',
pos: 0,
},
{
name: 'some column',
field: 'some column',
pos: 1,
},
],
client_primary_key: '__temp_PK',
};
$('body').append(deleteButton);
$('body').append(copyButton);
deleteButton.prop('disabled', true);
copyButton.prop('disabled', true);
selectionSpy = jasmine.createSpyObj('selectionSpy', [
'setSelectedRows',
'getSelectedRanges',
]);
});
afterEach(function () {
copyButton.remove();
deleteButton.remove();
});
describe('when no full rows are selected', function () {
describe('when nothing is selected', function () {
beforeEach(function () {
selectionSpy.getSelectedRanges.and.returnValue([]);
sqlEditorObj.selection = selectionSpy;
SetStagedRows.call(sqlEditorObj, {}, {});
});
it('should disable the delete row button', function () {
expect($('#btn-delete-row').prop('disabled')).toBeTruthy();
});
it('should disable the copy row button', function () {
expect($('#btn-copy-row').prop('disabled')).toBeTruthy();
});
it('should clear staged rows', function () {
expect(sqlEditorObj.editor.handler.data_store.staged_rows).toEqual({});
});
});
describe('when there is a selection', function () {
beforeEach(function () {
var range = {
fromCell: 0,
toCell: 0,
fromRow: 1,
toRow: 1,
};
selectionSpy.getSelectedRanges.and.returnValue([range]);
sqlEditorObj.selection = selectionSpy;
SetStagedRows.call(sqlEditorObj, {}, {});
});
it('should disable the delete row button', function () {
expect($('#btn-delete-row').prop('disabled')).toBeTruthy();
});
it('should disable the copy row button', function () {
expect($('#btn-copy-row').prop('disabled')).toBeFalsy();
});
it('should clear staged rows', function () {
expect(sqlEditorObj.editor.handler.data_store.staged_rows).toEqual({});
});
});
});
describe('when 2 full rows are selected', function () {
beforeEach(function () {
var range1 = {
fromCell: 0,
toCell: 1,
fromRow: 1,
toRow: 1,
};
var range2 = {
fromCell: 0,
toCell: 1,
fromRow: 2,
toRow: 2,
};
selectionSpy.getSelectedRanges.and.returnValue([range1, range2]);
sqlEditorObj.selection = selectionSpy;
});
describe('when table does not have primary keys', function () {
it('should enable the copy row button', function () {
SetStagedRows.call(sqlEditorObj, {}, {});
expect($('#btn-copy-row').prop('disabled')).toBeFalsy();
});
it('should not enable the delete row button', function () {
SetStagedRows.call(sqlEditorObj, {}, {});
expect($('#btn-delete-row').prop('disabled')).toBeTruthy();
});
it('should update staged rows with the __temp_PK value of the new Selected Rows', function () {
SetStagedRows.call(sqlEditorObj, {}, {});
expect(sqlEditorObj.editor.handler.data_store.staged_rows).toEqual({'456': {}, '789': {}});
});
describe('the user can edit', function () {
it('should enable the delete row button', function () {
sqlEditorObj.editor.handler.can_edit = true;
SetStagedRows.call(sqlEditorObj, {}, {});
expect($('#btn-delete-row').prop('disabled')).toBeFalsy();
});
});
});
describe('when table has primary keys', function () {
beforeEach(function () {
sqlEditorObj.keys = {'a pk column': 'varchar'};
sqlEditorObj.editor.handler.data_store.staged_rows = {'456': {'a pk column': 'three'}};
});
describe('selected rows have primary key', function () {
it('should set the staged rows correctly', function () {
SetStagedRows.call(sqlEditorObj, {}, {});
expect(sqlEditorObj.editor.handler.data_store.staged_rows).toEqual(
{'456': {'a pk column': 'three'}, '789': {'a pk column': 'five'}});
});
it('should not clear selected rows in Cell Selection Model', function () {
SetStagedRows.call(sqlEditorObj, {}, {});
expect(sqlEditorObj.selection.setSelectedRows).not.toHaveBeenCalledWith();
});
});
describe('selected rows missing primary key', function () {
beforeEach(function () {
var data = [{'a pk column': 'one', 'some column': 'two', '__temp_PK': '123'},
{'some column': 'four', '__temp_PK': '456'},
{'some column': 'six', '__temp_PK': '789'},
{'a pk column': 'seven', 'some column': 'eight', '__temp_PK': '432'}],
dataView = new Slick.Data.DataView();
dataView.setItems(data, '__temp_PK');
gridSpy.getData.and.returnValue(dataView);
});
it('should clear the staged rows', function () {
SetStagedRows.call(sqlEditorObj, {}, {});
expect(sqlEditorObj.editor.handler.data_store.staged_rows).toEqual({});
});
it('should clear selected rows in Cell Selection Model', function () {
SetStagedRows.call(sqlEditorObj, {}, {});
expect(sqlEditorObj.selection.setSelectedRows).toHaveBeenCalledWith([]);
});
});
describe('when the selected row is a new row', function () {
var parentDiv;
beforeEach(function () {
var childDiv = $('<div></div>');
parentDiv = $('<div class="new_row"></div>');
parentDiv.append(childDiv);
$('body').append(parentDiv);
gridSpy.getCellNode.and.returnValue(childDiv);
SetStagedRows.call(sqlEditorObj, {}, {});
});
afterEach(function () {
parentDiv.remove();
});
it('should not clear the staged rows', function () {
expect(sqlEditorObj.editor.handler.data_store.staged_rows).toEqual({
'456': {'a pk column': 'three'},
'789': {'a pk column': 'five'},
});
});
it('should not clear selected rows in Cell Selection Model', function () {
expect(sqlEditorObj.selection.setSelectedRows).not.toHaveBeenCalled();
});
});
});
});
});

View File

@ -1,518 +0,0 @@
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
import 'slickgrid.plugins/slick.cellrangeselector';
import XCellSelectionModel from 'sources/selection/xcell_selection_model';
import 'slickgrid.grid';
import Slick from 'slickgrid';
import $ from 'jquery';
describe('XCellSelectionModel', function () {
var KEY_RIGHT = 39;
var KEY_LEFT = 37;
var KEY_UP = 38;
var KEY_DOWN = 40;
var container, grid;
var SlickGrid = Slick.Grid;
var oldWindowParent = window.parent;
beforeEach(function () {
window.parent = window;
var columns = [{
id: 'row-header-column',
name: 'row header column name',
selectable: false,
}, {
id: '1',
name: 'some-column-name',
field: 'some-column-name',
pos: 0,
}, {
id: 'second-column-id',
name: 'second column',
field: 'second column',
pos: 1,
}, {
id: 'third-column-id',
name: 'third column',
field: 'third column',
pos: 2,
},
];
var data = [];
for (var i = 0; i < 10; i++) {
data.push({
'some-column-name': 'some-value-' + i,
'second column': 'second value ' + i,
'third column': 'third value ' + i,
'fourth column': 'fourth value ' + i,
'__temp_PK': '123' + i,
});
}
container = $('<div></div>');
var dataView = new Slick.Data.DataView();
container.height(9999);
container.width(9999);
dataView.setItems(data, '__temp_PK');
grid = new SlickGrid(container, dataView, columns);
grid.setSelectionModel(new XCellSelectionModel());
$('body').append(container);
});
afterEach(function () {
grid.destroy();
container.remove();
window.parent = oldWindowParent;
});
describe('handleKeyDown', function () {
describe('when we press a random key', function () {
it('should not change the range', function () {
var range = new Slick.Range(1, 2);
grid.setActiveCell(1, 2);
grid.getSelectionModel().setSelectedRanges([range]);
pressKey(72);
expect(grid.getSelectionModel().getSelectedRanges()[0]).toEqual(range);
});
});
describe('when we press an arrow key ', function () {
it('should select the cell to the right', function () {
var range = new Slick.Range(1, 2);
grid.setActiveCell(1, 2);
grid.getSelectionModel().setSelectedRanges([range]);
pressKey(KEY_RIGHT);
expectOneSelectedRange(1, 3, 1, 3);
});
});
describe('when we press shift', function () {
describe('and we press an arrow key', function () {
var scrollColumnIntoViewSpy, scrollRowIntoViewSpy;
beforeEach(function () {
scrollColumnIntoViewSpy = spyOn(grid, 'scrollColumnIntoView');
scrollRowIntoViewSpy = spyOn(grid, 'scrollRowIntoView');
});
describe('the right arrow', function () {
describe('when a cell is selected', function () {
beforeEach(function () {
var range = new Slick.Range(1, 1);
grid.setActiveCell(1, 1);
grid.getSelectionModel().setSelectedRanges([range]);
pressShiftPlusKey(KEY_RIGHT);
});
it('increases the range by one to the right', function () {
expectOneSelectedRange(1, 1, 1, 2);
});
it('should scroll the next column into view', function () {
expect(scrollColumnIntoViewSpy).toHaveBeenCalledWith(2);
expect(scrollRowIntoViewSpy).not.toHaveBeenCalled();
});
it('pressing right again grows the range right', function () {
pressShiftPlusKey(KEY_RIGHT);
expectOneSelectedRange(1, 1, 1, 3);
});
it('then pressing left keeps the original selection', function () {
pressShiftPlusKey(KEY_LEFT);
expectOneSelectedRange(1, 1, 1, 1);
});
});
describe('when a column is selected', function () {
beforeEach(function () {
var range = new Slick.Range(0, 1, 9, 1);
grid.setActiveCell(0, 1);
grid.getSelectionModel().setSelectedRanges([range]);
pressShiftPlusKey(KEY_RIGHT);
});
it('increases the range by one column to the right', function () {
expectOneSelectedRange(0, 1, 9, 2);
});
it('should scroll the next column into view', function () {
expect(scrollColumnIntoViewSpy).toHaveBeenCalledWith(2);
expect(scrollRowIntoViewSpy).not.toHaveBeenCalled();
});
});
});
describe('the left arrow', function () {
describe('when a cell is selected', function () {
beforeEach(function () {
var range = new Slick.Range(1, 3);
grid.setActiveCell(1, 3);
grid.getSelectionModel().setSelectedRanges([range]);
pressShiftPlusKey(KEY_LEFT);
});
it('increases the range by one to the left', function () {
expectOneSelectedRange(1, 2, 1, 3);
});
it('should scroll previous column into view', function () {
expect(scrollColumnIntoViewSpy).toHaveBeenCalledWith(2);
expect(scrollRowIntoViewSpy).not.toHaveBeenCalled();
});
it('pressing left again grows the range the left', function () {
pressShiftPlusKey(KEY_LEFT);
expectOneSelectedRange(1, 1, 1, 3);
});
it('then pressing right keeps the original selection', function () {
pressShiftPlusKey(KEY_RIGHT);
expectOneSelectedRange(1, 3, 1, 3);
});
});
describe('when a column is selected', function () {
beforeEach(function () {
var range = new Slick.Range(0, 2, 9, 2);
grid.setActiveCell(0, 2);
grid.getSelectionModel().setSelectedRanges([range]);
pressShiftPlusKey(KEY_LEFT);
});
it('increases the range by one column to the left', function () {
expectOneSelectedRange(0, 1, 9, 2);
});
it('should scroll previous column into view', function () {
expect(scrollColumnIntoViewSpy).toHaveBeenCalledWith(1);
expect(scrollRowIntoViewSpy).not.toHaveBeenCalled();
});
});
});
describe('the up arrow', function () {
describe('when a cell is selected', function () {
beforeEach(function () {
var range = new Slick.Range(2, 2);
grid.setActiveCell(2, 2);
grid.getSelectionModel().setSelectedRanges([range]);
pressShiftPlusKey(KEY_UP);
});
it('increases the range by one up', function () {
expectOneSelectedRange(1, 2, 2, 2);
});
it('should scroll the row above into view', function () {
expect(scrollRowIntoViewSpy).toHaveBeenCalledWith(1);
expect(scrollColumnIntoViewSpy).not.toHaveBeenCalled();
});
it('pressing up again grows the range up', function () {
pressShiftPlusKey(KEY_UP);
expectOneSelectedRange(0, 2, 2, 2);
});
it('then pressing down keeps the original selection', function () {
pressShiftPlusKey(KEY_DOWN);
expectOneSelectedRange(2, 2, 2, 2);
});
});
describe('when a row is selected', function () {
beforeEach(function () {
var range = new Slick.Range(2, 1, 2, 3);
grid.setActiveCell(2, 1);
grid.getSelectionModel().setSelectedRanges([range]);
pressShiftPlusKey(KEY_UP);
});
it('increases the range by one row up', function () {
expectOneSelectedRange(1, 1, 2, 3);
});
it('should scroll the row above into view', function () {
expect(scrollRowIntoViewSpy).toHaveBeenCalledWith(1);
expect(scrollColumnIntoViewSpy).not.toHaveBeenCalled();
});
});
});
describe('the down arrow', function () {
describe('when a cell is selected', function () {
beforeEach(function () {
var range = new Slick.Range(2, 2);
grid.setActiveCell(2, 2);
grid.getSelectionModel().setSelectedRanges([range]);
pressShiftPlusKey(KEY_DOWN);
});
it('increases the range by one down', function () {
expectOneSelectedRange(2, 2, 3, 2);
});
it('should scroll the row below into view', function () {
expect(scrollRowIntoViewSpy).toHaveBeenCalledWith(3);
expect(scrollColumnIntoViewSpy).not.toHaveBeenCalled();
});
it('pressing down again grows the range down', function () {
pressShiftPlusKey(KEY_DOWN);
expectOneSelectedRange(2, 2, 4, 2);
});
it('then pressing up keeps the original selection', function () {
pressShiftPlusKey(KEY_UP);
expectOneSelectedRange(2, 2, 2, 2);
});
});
describe('when a row is selected', function () {
beforeEach(function () {
var range = new Slick.Range(2, 1, 2, 3);
grid.setActiveCell(2, 1);
grid.getSelectionModel().setSelectedRanges([range]);
pressShiftPlusKey(KEY_DOWN);
});
it('increases the range by one row down', function () {
expectOneSelectedRange(2, 1, 3, 3);
});
it('should scroll the row below into view', function () {
expect(scrollRowIntoViewSpy).toHaveBeenCalledWith(3);
expect(scrollColumnIntoViewSpy).not.toHaveBeenCalled();
});
});
});
describe('rectangular selection works', function () {
it('in the down-and-rightward direction', function () {
var range = new Slick.Range(1, 1);
grid.setActiveCell(1, 1);
grid.getSelectionModel().setSelectedRanges([range]);
pressShiftPlusKey(KEY_DOWN);
pressShiftPlusKey(KEY_DOWN);
pressShiftPlusKey(KEY_DOWN);
pressShiftPlusKey(KEY_RIGHT);
pressShiftPlusKey(KEY_RIGHT);
expectOneSelectedRange(1, 1, 4, 3);
});
it('in the up-and-leftward direction', function () {
var range = new Slick.Range(4, 3);
grid.setActiveCell(4, 3);
grid.getSelectionModel().setSelectedRanges([range]);
pressShiftPlusKey(KEY_UP);
pressShiftPlusKey(KEY_UP);
pressShiftPlusKey(KEY_UP);
pressShiftPlusKey(KEY_LEFT);
pressShiftPlusKey(KEY_LEFT);
expectOneSelectedRange(1, 1, 4, 3);
});
it('in the up-and-rightward direction', function () {
var range = new Slick.Range(4, 1);
grid.setActiveCell(4, 1);
grid.getSelectionModel().setSelectedRanges([range]);
pressShiftPlusKey(KEY_UP);
pressShiftPlusKey(KEY_UP);
pressShiftPlusKey(KEY_UP);
pressShiftPlusKey(KEY_RIGHT);
pressShiftPlusKey(KEY_RIGHT);
expectOneSelectedRange(1, 1, 4, 3);
});
it('in the down-and-leftward direction', function () {
var range = new Slick.Range(1, 3);
grid.setActiveCell(1, 3);
grid.getSelectionModel().setSelectedRanges([range]);
pressShiftPlusKey(KEY_DOWN);
pressShiftPlusKey(KEY_DOWN);
pressShiftPlusKey(KEY_DOWN);
pressShiftPlusKey(KEY_LEFT);
pressShiftPlusKey(KEY_LEFT);
expectOneSelectedRange(1, 1, 4, 3);
});
});
describe('and we are on an edge', function () {
var range;
beforeEach(function () {
range = new Slick.Range(2, 1);
grid.setActiveCell(2, 1);
grid.getSelectionModel().setSelectedRanges([range]);
});
it('we still have the selected range before we arrowed', function () {
pressShiftPlusKey(KEY_LEFT);
expectOneSelectedRange(2, 1, 2, 1);
});
});
});
});
});
describe('when we drag and drop', function () {
var dd;
// We could not find an elegant way to calculate this value
// after changing window size we saw this was a constant value
var offsetLeftColumns = 100;
function cellTopPosition($cell, rowNumber) {
return $(grid.getCanvasNode()).offset().top + $cell[0].scrollHeight * rowNumber;
}
function cellLeftPosition(columnNumber) {
return $(grid.getCanvasNode()).offset().left + offsetLeftColumns * columnNumber;
}
beforeEach(function () {
var initialPosition = {cell: 3, row: 4};
var $cell = $($('.slick-cell.l3')[initialPosition.row]);
var event = {
target: $cell,
isPropagationStopped: jasmine.createSpy('isPropagationStopped').and.returnValue(false),
isImmediatePropagationStopped: jasmine.createSpy('isImmediatePropagationStopped').and.returnValue(false),
stopImmediatePropagation: jasmine.createSpy('stopImmediatePropagation'),
};
dd = {
grid: grid,
startX: cellLeftPosition(initialPosition.cell),
startY: cellTopPosition($cell, initialPosition.row),
};
grid.onDragStart.notify(dd, event, grid);
});
describe('when the drop happens outside of the grid', function () {
beforeEach(function () {
var $cell = $($('.slick-cell.l1')[1]);
var finalPosition = {cell: 1, row: 1};
var event = {
target: $cell,
isPropagationStopped: jasmine.createSpy('isPropagationStopped').and.returnValue(false),
isImmediatePropagationStopped: jasmine.createSpy('isImmediatePropagationStopped').and.returnValue(false),
stopImmediatePropagation: jasmine.createSpy('stopImmediatePropagation'),
pageX: cellLeftPosition(finalPosition.cell),
pageY: cellTopPosition($cell, finalPosition.row),
};
grid.onDrag.notify(dd, event, grid);
$(window).mouseup();
});
it('should call handleDragEnd from CellRangeSelector', function () {
var newRange = grid.getSelectionModel().getSelectedRanges();
expect(newRange.length).toEqual(1);
expect(newRange[0].fromCell).toEqual(1);
expect(newRange[0].toCell).toEqual(3);
expect(newRange[0].fromRow).toEqual(1);
expect(newRange[0].toRow).toEqual(4);
});
});
});
describe('when we mouse up and no drag and drop occured', function () {
beforeEach(function () {
grid.onDragEnd.notify = jasmine.createSpy('notify');
grid.onDragEnd.notify.calls.reset();
$(window).mouseup();
});
it('do not notify onDragEnd', function () {
expect(grid.onDragEnd.notify).not.toHaveBeenCalled();
});
});
describe('setSelectedRows', function () {
beforeEach(function () {
grid.getSelectionModel().setSelectedRanges(
[new Slick.Range(1, 1, 1, 1)]
);
});
describe('when passed an empty array', function () {
beforeEach(function () {
grid.getSelectionModel().setSelectedRows([]);
});
it('clears ranges', function () {
var newRanges = grid.getSelectionModel().getSelectedRanges();
expect(newRanges.length).toEqual(0);
});
});
it('sets ranges corresponding to rows', function () {
grid.getSelectionModel().setSelectedRows([0, 2]);
var selectedRanges = grid.getSelectionModel().getSelectedRanges();
expect(selectedRanges.length).toEqual(2);
expectRangeToMatch(selectedRanges[0], 0, 1, 0, 3);
expectRangeToMatch(selectedRanges[1], 2, 1, 2, 3);
});
});
function pressKey(keyCode) {
var pressEvent = new $.Event('keydown');
pressEvent.which = keyCode;
$(container.find('.grid-canvas')).trigger(pressEvent);
}
function pressShiftPlusKey(keyCode) {
var pressEvent = new $.Event('keydown');
pressEvent.shiftKey = true;
pressEvent.which = keyCode;
$(container.find('.grid-canvas')).trigger(pressEvent);
}
function expectOneSelectedRange(fromRow, fromCell, toRow, toCell) {
var selectedRanges = grid.getSelectionModel().getSelectedRanges();
expect(selectedRanges.length).toEqual(1);
expectRangeToMatch(selectedRanges[0], fromRow, fromCell, toRow, toCell);
}
function expectRangeToMatch(range, fromRow, fromCell, toRow, toCell) {
expect(range.fromRow).toEqual(fromRow);
expect(range.toRow).toEqual(toRow);
expect(range.fromCell).toEqual(fromCell);
expect(range.toCell).toEqual(toCell);
}
});

View File

@ -1,80 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import $ from 'jquery';
import 'slickgrid';
import 'slickgrid.grid';
import XCellSelectionModel from 'sources/selection/xcell_selection_model';
import CellSelector from 'sources/slickgrid/cell_selector';
import RangeSelectionHelper from 'sources/selection/range_selection_helper';
describe('CellSelector', function () {
var container, columns, cellSelector, data, cellSelectionModel, grid;
var Slick = window.Slick;
beforeEach(function () {
container = $('<div></div>');
container.height(9999);
container.width(9999);
columns = [{
name: 'some-column-name',
}, {
name: 'second column',
}];
cellSelector = new CellSelector();
data = [];
for (var i = 0; i < 10; i++) {
data.push({'some-column-name': 'some-value-' + i, 'second column': 'second value ' + i});
}
grid = new Slick.Grid(container, data, columns);
cellSelectionModel = new XCellSelectionModel();
grid.setSelectionModel(cellSelectionModel);
grid.registerPlugin(cellSelector);
grid.invalidate();
$('body').append(container);
});
afterEach(function () {
$('body').find(container).remove();
});
describe('when the user clicks or tabs to a cell', function () {
it('sets the selected range to that cell', function () {
var row = 1, column = 0;
$(container.find('.slick-row .slick-cell.l' + column)[row]).trigger('click');
var selectedRanges = cellSelectionModel.getSelectedRanges();
expect(selectedRanges.length).toEqual(1);
expect(selectedRanges[0].fromCell).toEqual(0);
expect(selectedRanges[0].toCell).toEqual(0);
expect(selectedRanges[0].fromRow).toEqual(1);
expect(selectedRanges[0].toRow).toEqual(1);
});
it('deselects previously selected ranges', function () {
var row2Range = RangeSelectionHelper.rangeForRow(grid, 2);
var ranges = RangeSelectionHelper.addRange(cellSelectionModel.getSelectedRanges(),
row2Range);
cellSelectionModel.setSelectedRanges(ranges);
var row = 4, column = 1;
$(container.find('.slick-row .slick-cell.l' + column)[row]).trigger('click');
expect(RangeSelectionHelper.isRangeSelected(cellSelectionModel.getSelectedRanges(), row2Range))
.toEqual(false);
});
});
});

View File

@ -1,125 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import HandleQueryOutputKeyboardEvent from 'sources/slickgrid/event_handlers/handle_query_output_keyboard_event';
import clipboard from 'sources/selection/clipboard';
import RangeSelectionHelper from 'sources/selection/range_selection_helper';
import XCellSelectionModel from 'sources/selection/xcell_selection_model';
import Slick from 'slickgrid';
import 'slickgrid.grid';
import $ from 'jquery';
describe('#handleQueryOutputKeyboardEvent', function () {
var event, grid, slickEvent;
var handleQueryOutputKeyboardEvent, buttonCopyWithHeader;
beforeEach(function () {
event = {
shiftKey: false,
ctrlKey: false,
metaKey: false,
which: -1,
keyCode: -1,
preventDefault: jasmine.createSpy('preventDefault'),
};
var data = [{'checkboxColumn': '', 'firstColumn': '0,0-cell-content', 'secondColumn': '0,1-cell-content', '__temp_PK': '123'},
{'checkboxColumn': '', 'firstColumn': '1,0-cell-content', 'secondColumn': '1,1-cell-content', '__temp_PK': '456'},
{'checkboxColumn': '', 'firstColumn': '2,0-cell-content', 'secondColumn': '2,1-cell-content', '__temp_PK': '789'}],
columnDefinitions = [{name: 'checkboxColumn'},
{pos: 1, name: 'firstColumn', field: 'firstColumn'},
{ pos: 2, name: 'secondColumn', field: 'secondColumn'}],
dataView = new Slick.Data.DataView(),
CSVOptions = {'quoting': 'all', 'quote_char': '\'', 'field_separator': ','};
grid = new Slick.Grid($('<div></div>'), dataView, columnDefinitions);
grid.setSelectionModel(new XCellSelectionModel());
grid.CSVOptions = CSVOptions;
dataView.setItems(data, '__temp_PK');
slickEvent = {
grid: grid,
};
buttonCopyWithHeader = $('<button class="copy-with-header visibility-hidden"></button>');
$('body').append(buttonCopyWithHeader);
spyOn(clipboard, 'copyTextToClipboard');
handleQueryOutputKeyboardEvent = HandleQueryOutputKeyboardEvent.bind(window);
});
let selectEntireGridAction = ()=> {
handleQueryOutputKeyboardEvent(event, slickEvent);
expect(RangeSelectionHelper.isEntireGridSelected(grid)).toBeTruthy();
expect(grid.getSelectionModel().getSelectedRanges().length).toEqual(1);
};
let commandAAction = ()=> {
beforeEach(function () {
event.metaKey = true;
event.keyCode = 65;
});
it('selects the entire grid to ranges', function () {
selectEntireGridAction();
});
};
let ctrlAAction = ()=> {
beforeEach(function () {
event.ctrlKey = true;
event.keyCode = 65;
});
it('selects the entire grid to ranges', function () {
selectEntireGridAction();
});
};
describe('when a range is selected', function () {
beforeEach(function () {
grid.getSelectionModel().setSelectedRanges([
RangeSelectionHelper.rangeForRow(grid, 0),
RangeSelectionHelper.rangeForRow(grid, 2),
]);
});
let copyCellContentAction = ()=> {
handleQueryOutputKeyboardEvent(event, slickEvent);
expect(clipboard.copyTextToClipboard).toHaveBeenCalledWith('\'0,0-cell-content\',\'0,1-cell-content\'\n\'2,0-cell-content\',\'2,1-cell-content\'');
};
describe('pressing Command + C', function () {
beforeEach(function () {
event.metaKey = true;
event.keyCode = 67;
});
it('copies the cell content to the clipboard', copyCellContentAction);
});
describe('pressing Ctrl + C', function () {
beforeEach(function () {
event.ctrlKey = true;
event.keyCode = 67;
});
it('copies the cell content to the clipboard', copyCellContentAction);
});
describe('pressing Command + A', commandAAction);
describe('pressing Ctrl + A', ctrlAAction);
});
describe('when no ranges are selected', function () {
describe('pressing Command + A', commandAAction);
describe('pressing Ctrl + A', ctrlAAction);
});
});

View File

@ -56,7 +56,7 @@ const providePlugin = new webpack.ProvidePlugin({
// Reference: https://webpack.js.org/plugins/source-map-dev-tool-plugin/#components/sidebar/sidebar.jsx
const sourceMapDevToolPlugin = new webpack.SourceMapDevToolPlugin({
filename: '[name].js.map',
exclude: /(vendor|codemirror|slickgrid|pgadmin\.js|pgadmin.theme|pgadmin.static|style\.js|popper)/,
exclude: /(vendor|codemirror|pgadmin\.js|pgadmin.theme|pgadmin.static|style\.js|popper)/,
columns: false,
});
@ -376,9 +376,8 @@ module.exports = [{
entry: {
'app.bundle': sourceDir + '/bundle/app.js',
codemirror: sourceDir + '/bundle/codemirror.js',
slickgrid: sourceDir + '/bundle/slickgrid.js',
sqleditor: './pgadmin/tools/sqleditor/static/js/index.js',
schema_diff: './pgadmin/tools/schema_diff/static/js/schema_diff_hook.js',
schema_diff: './pgadmin/tools/schema_diff/static/js/index.js',
erd_tool: './pgadmin/tools/erd/static/js/index.js',
psql_tool: './pgadmin/tools/psql/static/js/index.js',
debugger: './pgadmin/tools/debugger/static/js/index.js',

View File

@ -72,16 +72,6 @@ var webpackShimConfig = {
'deps': ['jquery'], 'exports': 'jQuery.fn.drag',
},
'jquery.ui': {'deps': ['jquery']},
'slick.pgadmin.formatters': {
'deps': ['slickgrid'],
},
'slick.pgadmin.editors': {
'deps': ['slickgrid'],
},
'slickgrid': {
'deps': ['jquery', 'jquery.ui', 'jquery.event.drag'],
'exports': 'Slick',
},
'alertify': {
'exports': 'alertify',
},
@ -132,8 +122,6 @@ var webpackShimConfig = {
'wcdocker': path.join(__dirname, './node_modules/webcabin-docker/Build/wcDocker.min'),
'alertify': path.join(__dirname, './node_modules/alertifyjs/build/alertify'),
'moment': path.join(__dirname, './node_modules/moment/moment'),
'jquery.event.drag': path.join(__dirname, './node_modules/slickgrid/lib/jquery.event.drag-2.3.0'),
'jquery.ui': path.join(__dirname, './node_modules/slickgrid/lib/jquery-ui-1.11.3.min'),
'jqueryui.position': path.join(__dirname, './node_modules/jquery-contextmenu/dist/jquery.ui.position'),
'jquery.contextmenu': path.join(__dirname, './node_modules/jquery-contextmenu/dist/jquery.contextMenu'),
'dropzone': path.join(__dirname, './node_modules/dropzone/dist/dropzone'),
@ -278,8 +266,7 @@ var webpackShimConfig = {
'pgadmin.tools.import_export_servers': path.join(__dirname, './pgadmin/tools/import_export_servers/static/js/'),
'pgadmin.tools.maintenance': path.join(__dirname, './pgadmin/tools/maintenance/static/js/maintenance'),
'pgadmin.tools.restore': path.join(__dirname, './pgadmin/tools/restore/static/js/restore'),
'pgadmin.tools.schema_diff': path.join(__dirname, './pgadmin/tools/schema_diff/static/js/schema_diff'),
'pgadmin.tools.schema_diff_ui': path.join(__dirname, './pgadmin/tools/schema_diff/static/js/schema_diff_ui'),
'pgadmin.tools.schema_diff': path.join(__dirname, './pgadmin/tools/schema_diff/static/js/'),
'pgadmin.tools.search_objects': path.join(__dirname, './pgadmin/tools/search_objects/static/js'),
'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'),
@ -287,9 +274,6 @@ var webpackShimConfig = {
'pgadmin.tools.sqleditor': path.join(__dirname, './pgadmin/tools/sqleditor/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',
'slick.pgadmin.editors': path.join(__dirname, './pgadmin/tools/../static/js/slickgrid/editors'),
'slick.pgadmin.formatters': path.join(__dirname, './pgadmin/tools/../static/js/slickgrid/formatters'),
'slick.pgadmin.plugins': path.join(__dirname, './pgadmin/tools/../static/js/slickgrid/plugins'),
},
externals: [
'pgadmin.user_management.current_user',
@ -306,8 +290,7 @@ var webpackShimConfig = {
'pgadmin.browser.error', 'pgadmin.browser.server.privilege',
'pgadmin.browser.server.variable', 'pgadmin.browser.collection', 'pgadmin.browser.node.ui',
'pgadmin.browser.datamodel', 'pgadmin.browser.menu', 'pgadmin.browser.panel', 'pgadmin',
'pgadmin.browser.frame', 'slick.pgadmin.editors', 'slick.pgadmin.formatters',
'pgadmin.backform', 'pgadmin.backgrid', 'pgadmin.browser',
'pgadmin.browser.frame', 'pgadmin.backform', 'pgadmin.backgrid', 'pgadmin.browser',
'pgadmin.browser.node',
'pgadmin.alertifyjs', 'pgadmin.settings', 'pgadmin.preferences', 'pgadmin.sqlfoldcode',
],

View File

@ -13,7 +13,6 @@ const webpack = require('webpack');
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
const sourcesDir = path.resolve(__dirname, 'pgadmin/static');
const nodeModulesDir = path.resolve(__dirname, 'node_modules');
const regressionDir = path.resolve(__dirname, 'regression');
module.exports = {
@ -99,62 +98,6 @@ module.exports = {
filename: 'img/[name].[ext]',
},
exclude: /vendor/,
}, {
test: /.*slickgrid[\\\/]+slick\.(?!core)*/,
use:[
{
loader: 'imports-loader',
options: {
type: 'commonjs',
imports: [
'pure|jquery.ui',
'pure|jquery.event.drag',
'pure|slickgrid',
],
},
},
],
}, {
test: /.*slickgrid\.plugins[\\\/]+slick\.cellrangeselector/,
use:[
{
loader: 'imports-loader',
options: {
type: 'commonjs',
imports: [
'pure|jquery.ui',
'pure|jquery.event.drag',
'pure|slickgrid',
],
},
}, {
loader: 'exports-loader',
options: {
type: 'commonjs',
exports: 'single|Slick.CellRangeSelector',
},
},
],
}, {
test: /.*slickgrid[\\\/]+slick\.core.*/,
use:[
{
loader: 'imports-loader',
options: {
type: 'commonjs',
imports: [
'pure|jquery.ui',
'pure|jquery.event.drag',
],
},
}, {
loader: 'exports-loader',
options: {
type: 'commonjs',
exports: 'single|Slick',
},
},
],
},
{
test: /\.js$|\.jsx$/,
@ -163,7 +106,7 @@ module.exports = {
options: { esModules: true },
},
enforce: 'post',
exclude: /node_modules|slickgrid|plugins|bundle|generated|regression|[Tt]est.js|[Ss]pecs.js|[Ss]pec.js|\.spec\.js$/,
exclude: /node_modules|plugins|bundle|generated|regression|[Tt]est.js|[Ss]pecs.js|[Ss]pec.js|\.spec\.js$/,
},
],
},
@ -175,8 +118,6 @@ module.exports = {
'jquery': path.join(__dirname, './node_modules/jquery/dist/jquery'),
'wcdocker': path.join(__dirname, './node_modules/webcabin-docker/Build/wcDocker'),
'alertify': path.join(__dirname, './node_modules/alertifyjs/build/alertify'),
'jquery.event.drag': path.join(__dirname, './node_modules/slickgrid/lib/jquery.event.drag-2.3.0'),
'jquery.ui': path.join(__dirname, './node_modules/slickgrid/lib/jquery-ui-1.11.3'),
'color-picker': path.join(__dirname, './node_modules/@simonwep/pickr/dist/pickr.min'),
'bignumber': path.join(__dirname, './node_modules/bignumber.js/bignumber'),
'bootstrap.datetimepicker': path.join(__dirname, './node_modules/tempusdominus-bootstrap-4/build/js/tempusdominus-bootstrap-4.min'),
@ -192,9 +133,6 @@ module.exports = {
'pgadmin.browser.messages': regressionDir + '/javascript/fake_messages',
'pgadmin.server.supported_servers': regressionDir + '/javascript/fake_supported_servers',
'pgadmin.browser.endpoints': regressionDir + '/javascript/fake_endpoints',
'slickgrid': nodeModulesDir + '/slickgrid/',
'slickgrid.plugins': nodeModulesDir + '/slickgrid/plugins/',
'slickgrid.grid': nodeModulesDir + '/slickgrid/slick.grid',
'moment': path.join(__dirname, './node_modules/moment/moment'),
'jsoneditor.min': path.join(__dirname, './node_modules/jsoneditor/dist/jsoneditor.min'),
'browser': path.resolve(__dirname, 'pgadmin/browser/static/js'),

View File

@ -7121,14 +7121,14 @@ jquery-contextmenu@^2.6.4, jquery-contextmenu@^2.9.2:
dependencies:
jquery "^3.5.0"
jquery-ui@>=1.8.0, jquery-ui@^1.13.2:
jquery-ui@^1.13.2:
version "1.13.2"
resolved "https://registry.yarnpkg.com/jquery-ui/-/jquery-ui-1.13.2.tgz#de03580ae6604773602f8d786ad1abfb75232034"
integrity sha512-wBZPnqWs5GaYJmo1Jj0k/mrSkzdQzKDwhXNtHKcBdAcKVxMM3KNYFq+iJ2i1rwiG53Z8M4mTn3Qxrm17uH1D4Q==
dependencies:
jquery ">=1.8.0 <4.0.0"
"jquery@>=1.7.1 <4.0.0", jquery@>=1.8.0, "jquery@>=1.8.0 <4.0.0", jquery@^3.3.1, jquery@^3.5.0, jquery@^3.5.1, jquery@^3.6.0:
"jquery@>=1.7.1 <4.0.0", "jquery@>=1.8.0 <4.0.0", jquery@^3.3.1, jquery@^3.5.0, jquery@^3.5.1, jquery@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.0.tgz#c72a09f15c1bdce142f49dbf1170bdf8adac2470"
integrity sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==
@ -10365,13 +10365,6 @@ slice-ansi@^4.0.0:
astral-regex "^2.0.0"
is-fullwidth-code-point "^3.0.0"
"slickgrid@git+https://github.com/6pac/SlickGrid.git#2.3.16":
version "2.3.16"
resolved "git+https://github.com/6pac/SlickGrid.git#4f8c6f498d0b82391fdf382beb8ef114ed7408e7"
dependencies:
jquery ">=1.8.0"
jquery-ui ">=1.8.0"
smart-buffer@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"