mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-01-24 07:16:52 -06:00
627 lines
16 KiB
JavaScript
627 lines
16 KiB
JavaScript
//////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// pgAdmin 4 - PostgreSQL Tools
|
|
//
|
|
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
|
// This software is released under the PostgreSQL Licence
|
|
//
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
import _ from 'lodash';
|
|
import pgAdmin from 'sources/pgadmin';
|
|
|
|
import { FileType } from 'react-aspen';
|
|
import { TreeNode } from './tree_nodes';
|
|
|
|
function manageTreeEvents(event, eventName, item) {
|
|
let d = item ? item._metadata.data : [];
|
|
let node_metadata = item ? item._metadata : {};
|
|
let node;
|
|
let obj = pgAdmin.Browser;
|
|
|
|
// Events for preferences tree.
|
|
if (node_metadata.parent?.includes('/preferences') && obj.ptree.tree.type == 'preferences') {
|
|
try {
|
|
obj.Events.trigger(
|
|
'preferences:tree:' + eventName, event, item, d
|
|
);
|
|
} catch (e) {
|
|
console.warn(e.stack || e);
|
|
return false;
|
|
}
|
|
} else if(eventName == 'hovered') {
|
|
/* Raise tree events for the nodes */
|
|
try {
|
|
obj.Events.trigger(
|
|
'pgadmin-browser:tree:' + eventName, item, d, node
|
|
);
|
|
} catch (e) {
|
|
console.warn(e.stack || e);
|
|
return false;
|
|
}
|
|
} else if (d && obj.Nodes[d._type]) {
|
|
// Events for browser tree.
|
|
node = obj.Nodes[d._type];
|
|
|
|
// If the Browser tree is not initialised yet
|
|
if (obj.tree === null) return;
|
|
|
|
if (eventName == 'dragstart') {
|
|
obj.tree.handleDraggable(event, item);
|
|
}
|
|
if (eventName == 'added' || eventName == 'beforeopen' || eventName == 'loaded') {
|
|
obj.tree.addNewNode(item.getMetadata('data').id, item.getMetadata('data'), item, item.parent.path);
|
|
}
|
|
if(eventName == 'copied') {
|
|
obj.tree.copyHandler?.(item.getMetadata('data'), item);
|
|
}
|
|
if (_.isObject(node.callbacks) &&
|
|
eventName in node.callbacks &&
|
|
typeof node.callbacks[eventName] == 'function') {
|
|
node.callbacks[eventName].apply(node, [item, d, obj, [], eventName]);
|
|
}
|
|
|
|
/* Raise tree events for the nodes */
|
|
try {
|
|
obj.Events.trigger(
|
|
'pgadmin-browser:tree:' + eventName, item, d, node
|
|
);
|
|
} catch (e) {
|
|
console.warn(e.stack || e);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
export class Tree {
|
|
constructor(tree, manageTree, pgBrowser, type) {
|
|
this.tree = tree;
|
|
this.tree.type = type || 'browser';
|
|
this.tree.onTreeEvents(manageTreeEvents);
|
|
|
|
this.rootNode = manageTree.tempTree;
|
|
this.Nodes = pgBrowser ? pgBrowser.Nodes : pgAdmin.Browser.Nodes;
|
|
|
|
this.draggableTypes = {};
|
|
}
|
|
|
|
async refresh(item) {
|
|
// Set _children to null as empty array not reload the children nodes on refresh.
|
|
if(item.children?.length == 0) {
|
|
item._children = null;
|
|
}
|
|
await this.tree.refresh(item);
|
|
}
|
|
|
|
async add(item, data) {
|
|
await this.tree.create(item.parent, data.itemData);
|
|
}
|
|
|
|
async before(item, data) {
|
|
return Promise.resolve(await this.tree.create(item.parent, data));
|
|
}
|
|
|
|
async update(item, data) {
|
|
await this.tree.update(item, data);
|
|
}
|
|
|
|
async remove(item) {
|
|
await this.tree.remove(item);
|
|
}
|
|
|
|
async append(item, data) {
|
|
return Promise.resolve(await this.tree.create(item, data));
|
|
}
|
|
|
|
async destroy() {
|
|
const model = this.tree.getModel();
|
|
this.rootNode.children = [];
|
|
if (model.root) {
|
|
model.root.isExpanded = false;
|
|
return Promise.resolve(await model.root.hardReloadChildren());
|
|
}
|
|
}
|
|
|
|
next(item) {
|
|
if (item) {
|
|
let parent = this.parent(item);
|
|
if (parent && parent.children.length > 0) {
|
|
let idx = parent.children.indexOf(item);
|
|
if (idx !== -1 && parent.children.length !== idx + 1) {
|
|
return parent.children[idx + 1];
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
prev(item) {
|
|
if (item) {
|
|
let parent = this.parent(item);
|
|
if (parent && parent.children.length > 0) {
|
|
let idx = parent.children.indexOf(item);
|
|
if (idx !== -1 && idx !== 0) {
|
|
return parent.children[idx - 1];
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async open(item) {
|
|
if (this.isOpen(item)) { return true; }
|
|
await this.tree.toggleDirectory(item);
|
|
}
|
|
|
|
async ensureLoaded(item) {
|
|
await item.ensureLoaded();
|
|
}
|
|
|
|
async ensureVisible(item, align='auto') {
|
|
await this.tree.ensureVisible(item, align);
|
|
}
|
|
|
|
async openPath(item) {
|
|
parent = item.parent;
|
|
await this.tree.openDirectory(parent);
|
|
}
|
|
|
|
async close(item) {
|
|
await this.tree.closeDir(item);
|
|
}
|
|
|
|
async toggle(item) {
|
|
await this.tree.toggleDirectory(item);
|
|
}
|
|
|
|
async select(item, ensureVisible = false, align = 'auto') {
|
|
await this.tree.setActiveFile(item, ensureVisible, align);
|
|
}
|
|
|
|
async selectNode(item, ensureVisible = false, align = 'auto') {
|
|
this.tree.setActiveFile(item, ensureVisible, align);
|
|
}
|
|
|
|
async unload(item) {
|
|
await this.tree.unload(item);
|
|
}
|
|
|
|
async addIcon(item, icon) {
|
|
if (item?.getMetadata('data') !== undefined) {
|
|
item.getMetadata('data').icon = icon.icon;
|
|
}
|
|
await this.tree.addIcon(item, icon);
|
|
}
|
|
|
|
removeIcon() {
|
|
// TBD
|
|
}
|
|
|
|
setLeaf() {
|
|
// TBD
|
|
}
|
|
async setLabel(item, label) {
|
|
if (item) {
|
|
await this.tree.setLabel(item, label);
|
|
}
|
|
}
|
|
|
|
async setInode(item) {
|
|
if (item._children) item._children = null;
|
|
await this.tree.closeDirectory(item);
|
|
}
|
|
|
|
async setId(item, data) {
|
|
if (item) {
|
|
item.getMetadata('data').id = data.id;
|
|
}
|
|
}
|
|
|
|
async deselect(item) {
|
|
await this.tree.deSelectActiveFile(item);
|
|
}
|
|
|
|
wasInit() {
|
|
// TBD
|
|
return true;
|
|
}
|
|
|
|
wasLoad(item) {
|
|
if (item?.type === FileType.Directory) {
|
|
return item.isExpanded && item.children != null && item.children.length > 0;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
parent(item) {
|
|
return item.parent;
|
|
}
|
|
|
|
first(item) {
|
|
const model = this.tree.getModel();
|
|
if ((item === undefined || item === null) && model.root.children !== null) {
|
|
return model.root.children[0];
|
|
}
|
|
|
|
if (item?.branchSize > 0) {
|
|
return item.children[0];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
children(item) {
|
|
const model = this.tree.getModel();
|
|
if (item) {
|
|
return (item.children !== null ? item.children : []);
|
|
}
|
|
return model.root.children;
|
|
}
|
|
|
|
itemFrom(domElem) {
|
|
return this.tree.getItemFromDOM(domElem);
|
|
}
|
|
|
|
DOMFrom(item) {
|
|
return this.tree.getDOMFromItem(item);
|
|
}
|
|
|
|
addCssClass(item, cssClass) {
|
|
this.tree.addCssClass(item, cssClass);
|
|
}
|
|
|
|
path(item) {
|
|
if (item) return item.path;
|
|
}
|
|
|
|
pathId(item) {
|
|
if (item) {
|
|
let pathIds = item.path.split('/');
|
|
pathIds.splice(0, 1);
|
|
return pathIds;
|
|
}
|
|
return [];
|
|
}
|
|
|
|
itemFromDOM(domElem) {
|
|
return this.tree.getItemFromDOM(domElem[0]);
|
|
}
|
|
|
|
siblings(item) {
|
|
if (this.hasParent(item)) {
|
|
let _siblings = this.parent(item).children.filter((_item) => _item.path !== item.path);
|
|
if (typeof (_siblings) !== 'object') return [_siblings];
|
|
else return _siblings;
|
|
}
|
|
return [];
|
|
}
|
|
|
|
hasParent(item) {
|
|
return item?.parent;
|
|
}
|
|
|
|
isOpen(item) {
|
|
if (item.type === FileType.Directory) {
|
|
return item.isExpanded;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
isClosed(item) {
|
|
if (item.type === FileType.Directory) {
|
|
return !item.isExpanded;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
itemData(item) {
|
|
return (item?.getMetadata('data') !== undefined) ? item?._metadata.data : [];
|
|
}
|
|
|
|
getData(item) {
|
|
return (item?.getMetadata('data') !== undefined) ? item?._metadata.data : [];
|
|
}
|
|
|
|
isRootNode(item) {
|
|
const model = this.tree.getModel();
|
|
return item === model.root;
|
|
}
|
|
|
|
isInode(item) {
|
|
const children = this.children(item);
|
|
if (children === null || children === undefined) return false;
|
|
return children.length > 0;
|
|
}
|
|
|
|
selected() {
|
|
return this.tree.getActiveFile();
|
|
}
|
|
|
|
resizeTree() {
|
|
this.tree.resize();
|
|
}
|
|
|
|
findNodeWithToggle(path) {
|
|
let tree = this;
|
|
|
|
if (path == null || !Array.isArray(path)) {
|
|
return Promise.reject(new Error(null));
|
|
}
|
|
const basepath = '/browser/' + path.slice(0, path.length-1).join('/') + '/';
|
|
path = '/browser/' + path.join('/');
|
|
|
|
let onCorrectPath = function (matchPath) {
|
|
return (matchPath !== undefined && path !== undefined
|
|
&& (basepath.startsWith(`${matchPath}/`) || path === matchPath));
|
|
};
|
|
|
|
return (function findInNode(currentNode) {
|
|
return new Promise((resolve, reject) => {
|
|
if (path === null || path === undefined || path.length === 0) {
|
|
resolve(null);
|
|
}
|
|
/* No point in checking the children if
|
|
* the path for currentNode itself is not matching
|
|
*/
|
|
if (currentNode.path !== undefined && !onCorrectPath(currentNode.path)) {
|
|
reject(new Error(null));
|
|
} else if (currentNode.path === path) {
|
|
resolve(currentNode);
|
|
} else {
|
|
tree.open(currentNode)
|
|
.then(() => {
|
|
let children = currentNode.children;
|
|
for (let i = 0, length = children.length; i < length; i++) {
|
|
let childNode = children[i];
|
|
if (onCorrectPath(childNode.path)) {
|
|
resolve(findInNode(childNode));
|
|
return;
|
|
}
|
|
}
|
|
reject(new Error(null));
|
|
})
|
|
.catch(() => {
|
|
reject(new Error(null));
|
|
});
|
|
}
|
|
});
|
|
})(tree.tree.getModel().root);
|
|
}
|
|
|
|
getNodeDisplayPath(item, separator='/', skip_coll=false) {
|
|
let retStack = [];
|
|
let currItem = item;
|
|
while(currItem?.fileName) {
|
|
const data = currItem._metadata?.data;
|
|
if(data._type.startsWith('coll-') && skip_coll) {
|
|
/* Skip collection */
|
|
} else {
|
|
retStack.push(data._label);
|
|
}
|
|
currItem = currItem.parent;
|
|
}
|
|
retStack = retStack.reverse();
|
|
if(!separator) return retStack;
|
|
return retStack.join(separator);
|
|
}
|
|
|
|
findNodeByDomElement(domElement) {
|
|
const path = domElement?.path;
|
|
if (!path?.[0]) {
|
|
return undefined;
|
|
}
|
|
|
|
return this.findNode(path);
|
|
}
|
|
|
|
addNewNode(id, data, item, parentPath) {
|
|
let parent;
|
|
parent = this.findNode(parentPath);
|
|
return this.createOrUpdateNode(id, data, parent, item);
|
|
}
|
|
|
|
findNode(path) {
|
|
if (path === null || path === undefined || path.length === 0 || path == '/browser') {
|
|
return this.rootNode;
|
|
}
|
|
return findInTree(this.rootNode, path);
|
|
}
|
|
|
|
createOrUpdateNode(id, data, parent, domNode) {
|
|
let oldNodePath = id;
|
|
if (parent?.path != '/browser') {
|
|
oldNodePath = parent.path + '/' + id;
|
|
}
|
|
const oldNode = this.findNode(oldNodePath);
|
|
if (oldNode !== null) {
|
|
oldNode.data = data;
|
|
oldNode.domNode = domNode;
|
|
return oldNode;
|
|
}
|
|
|
|
const node = new TreeNode(id, data, domNode, parent);
|
|
if (parent === this.rootNode) {
|
|
node.parentNode = null;
|
|
}
|
|
|
|
if (parent !== null && parent !== undefined)
|
|
parent.children.push(node);
|
|
return node;
|
|
}
|
|
|
|
async updateAndReselectNode(item, data) {
|
|
await this.update(item, data);
|
|
await this.deselect(item);
|
|
await this.select(item);
|
|
}
|
|
|
|
translateTreeNodeIdFromReactTree(treeNode) {
|
|
let currentTreeNode = treeNode;
|
|
let path = [];
|
|
while (currentTreeNode !== null && currentTreeNode !== undefined) {
|
|
if (currentTreeNode.path !== '/browser') path.unshift(currentTreeNode.path);
|
|
if (this.hasParent(currentTreeNode)) {
|
|
currentTreeNode = this.parent(currentTreeNode);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
return path;
|
|
}
|
|
|
|
getTreeNodeHierarchy(identifier) {
|
|
let idx = 0;
|
|
let node_cnt = 0;
|
|
let result = {};
|
|
if (!identifier) return;
|
|
let item = TreeNode.prototype.isPrototypeOf(identifier) ? identifier : this.findNode(identifier.path);
|
|
if (!item) return;
|
|
do {
|
|
const currentNodeData = item.getData();
|
|
if (currentNodeData._type in this.Nodes && this.Nodes[currentNodeData._type].hasId) {
|
|
const nodeType = mapType(currentNodeData._type, node_cnt);
|
|
if (result[nodeType] === undefined) {
|
|
result[nodeType] = _.extend({}, currentNodeData, {
|
|
'priority': idx,
|
|
});
|
|
idx -= 1;
|
|
}
|
|
}
|
|
node_cnt += 1;
|
|
item = item.hasParent() ? item.parent() : null;
|
|
} while (item);
|
|
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
*
|
|
* The dropDetailsFunc should return an object of sample
|
|
* {text: 'xyz', cur: {from:0, to:0} where text is the drop text and
|
|
* cur is selection range of text after dropping. If returned as
|
|
* string, by default cursor will be set to the end of text
|
|
*/
|
|
registerDraggableType(typeOrTypeDict, dropDetailsFunc = null) {
|
|
if (typeof typeOrTypeDict == 'object') {
|
|
Object.keys(typeOrTypeDict).forEach((type) => {
|
|
this.registerDraggableType(type, typeOrTypeDict[type]);
|
|
});
|
|
} else if (dropDetailsFunc != null) {
|
|
typeOrTypeDict.replace(/ +/, ' ').split(' ').forEach((type) => {
|
|
this.draggableTypes[type] = dropDetailsFunc;
|
|
});
|
|
}
|
|
}
|
|
|
|
getDraggable(type) {
|
|
if (this.draggableTypes[type]) {
|
|
return this.draggableTypes[type];
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
handleDraggable(e, item) {
|
|
let data = item.getMetadata('data');
|
|
let dropDetailsFunc = this.getDraggable(data._type);
|
|
|
|
if (dropDetailsFunc != null) {
|
|
let dropDetails = dropDetailsFunc(data, item, this.getTreeNodeHierarchy(item));
|
|
|
|
if (typeof dropDetails == 'string') {
|
|
dropDetails = {
|
|
text: dropDetails,
|
|
cur: {
|
|
from: dropDetails.length,
|
|
to: dropDetails.length,
|
|
},
|
|
};
|
|
} else if (!dropDetails.cur) {
|
|
dropDetails = {
|
|
...dropDetails,
|
|
cur: {
|
|
from: dropDetails.text.length,
|
|
to: dropDetails.text.length,
|
|
},
|
|
};
|
|
}
|
|
|
|
e.dataTransfer.setData('text', JSON.stringify(dropDetails));
|
|
/* Required by Firefox */
|
|
if (e.dataTransfer.dropEffect) {
|
|
e.dataTransfer.dropEffect = 'move';
|
|
}
|
|
|
|
/* setDragImage is not supported in IE. We leave it to
|
|
* its default look and feel
|
|
*/
|
|
const dropText = _.escape(dropDetails.text);
|
|
if(!dropText) {
|
|
e.preventDefault();
|
|
}
|
|
if (e.dataTransfer.setDragImage) {
|
|
const dragItem = document.createElement('div');
|
|
dragItem.classList.add('drag-tree-node');
|
|
dragItem.innerHTML = `<span>${dropText}</span>`;
|
|
|
|
document.querySelector('body .drag-tree-node')?.remove();
|
|
document.body.appendChild(dragItem);
|
|
|
|
e.dataTransfer.setDragImage(dragItem, 0, 0);
|
|
}
|
|
}
|
|
else {
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
|
|
onNodeCopy(copyCallback) {
|
|
this.copyHandler = copyCallback;
|
|
}
|
|
}
|
|
|
|
function mapType(type, idx) {
|
|
return (type === 'partition' && idx > 0) ? 'table' : type;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Given an initial node and a path, it will navigate through
|
|
* the new tree to find the node that matches the path
|
|
*/
|
|
export function findInTree(rootNode, path) {
|
|
if (path === null) {
|
|
return rootNode;
|
|
}
|
|
return (function findInNode(currentNode) {
|
|
|
|
/* No point in checking the children if
|
|
* the path for currentNode itself is not matching
|
|
*/
|
|
if (currentNode.path !== undefined && path !== undefined
|
|
&& !path.startsWith(currentNode.path)) {
|
|
return null;
|
|
}
|
|
|
|
for (let i = 0, length = currentNode.children.length; i < length; i++) {
|
|
const calculatedNode = findInNode(currentNode.children[i]);
|
|
if (calculatedNode !== null) {
|
|
return calculatedNode;
|
|
}
|
|
}
|
|
|
|
if (currentNode.path === path) {
|
|
return currentNode;
|
|
} else {
|
|
return null;
|
|
}
|
|
})(rootNode);
|
|
}
|
|
|
|
const isValidTreeNodeData = (data) => (!_.isEmpty(data));
|
|
|
|
export { isValidTreeNodeData };
|