codekingpro/portable-devtools
114k
1//////////////////////////////////////////////////////////////////////////2//3// pgAdmin 4 - PostgreSQL Tools4//5// Copyright (C) 2013 - 2024, The pgAdmin Development Team6// This software is released under the PostgreSQL Licence7//8//////////////////////////////////////////////////////////////////////////9 10import _ from 'lodash';11import pgAdmin from 'sources/pgadmin';12 13import { FileType } from 'react-aspen';14import { TreeNode } from './tree_nodes';15 16function manageTreeEvents(event, eventName, item) {17 let d = item ? item._metadata.data : [];18 let node_metadata = item ? item._metadata : {};19 let node;20 let obj = pgAdmin.Browser;21 22 // Events for preferences tree.23 if (node_metadata.parent?.includes('/preferences') && obj.ptree.tree.type == 'preferences') {24 try {25 obj.Events.trigger(26 'preferences:tree:' + eventName, event, item, d27 );28 } catch (e) {29 console.warn(e.stack || e);30 return false;31 }32 } else if(eventName == 'hovered') {33 /* Raise tree events for the nodes */34 try {35 obj.Events.trigger(36 'pgadmin-browser:tree:' + eventName, item, d, node37 );38 } catch (e) {39 console.warn(e.stack || e);40 return false;41 }42 } else if (d && obj.Nodes[d._type]) {43 // Events for browser tree.44 node = obj.Nodes[d._type];45 46 // If the Browser tree is not initialised yet47 if (obj.tree === null) return;48 49 if (eventName == 'dragstart') {50 obj.tree.handleDraggable(event, item);51 }52 if (eventName == 'added' || eventName == 'beforeopen' || eventName == 'loaded') {53 obj.tree.addNewNode(item.getMetadata('data').id, item.getMetadata('data'), item, item.parent.path);54 }55 if(eventName == 'copied') {56 obj.tree.copyHandler?.(item.getMetadata('data'), item);57 }58 if (_.isObject(node.callbacks) &&59 eventName in node.callbacks &&60 typeof node.callbacks[eventName] == 'function') {61 node.callbacks[eventName].apply(node, [item, d, obj, [], eventName]);62 }63 64 /* Raise tree events for the nodes */65 try {66 obj.Events.trigger(67 'pgadmin-browser:tree:' + eventName, item, d, node68 );69 } catch (e) {70 console.warn(e.stack || e);71 return false;72 }73 }74 return true;75}76 77 78export class Tree {79 constructor(tree, manageTree, pgBrowser, type) {80 this.tree = tree;81 this.tree.type = type || 'browser';82 this.tree.onTreeEvents(manageTreeEvents);83 84 this.rootNode = manageTree.tempTree;85 this.Nodes = pgBrowser ? pgBrowser.Nodes : pgAdmin.Browser.Nodes;86 87 this.draggableTypes = {};88 }89 90 async refresh(item) {91 // Set _children to null as empty array not reload the children nodes on refresh.92 if(item.children?.length == 0) {93 item._children = null;94 }95 await this.tree.refresh(item);96 }97 98 async add(item, data) {99 await this.tree.create(item.parent, data.itemData);100 }101 102 async before(item, data) {103 return Promise.resolve(await this.tree.create(item.parent, data));104 }105 106 async update(item, data) {107 await this.tree.update(item, data);108 }109 110 async remove(item) {111 await this.tree.remove(item);112 }113 114 async append(item, data) {115 return Promise.resolve(await this.tree.create(item, data));116 }117 118 async destroy() {119 const model = this.tree.getModel();120 this.rootNode.children = [];121 if (model.root) {122 model.root.isExpanded = false;123 return Promise.resolve(await model.root.hardReloadChildren());124 }125 }126 127 next(item) {128 if (item) {129 let parent = this.parent(item);130 if (parent && parent.children.length > 0) {131 let idx = parent.children.indexOf(item);132 if (idx !== -1 && parent.children.length !== idx + 1) {133 return parent.children[idx + 1];134 }135 }136 }137 return null;138 }139 140 prev(item) {141 if (item) {142 let parent = this.parent(item);143 if (parent && parent.children.length > 0) {144 let idx = parent.children.indexOf(item);145 if (idx !== -1 && idx !== 0) {146 return parent.children[idx - 1];147 }148 }149 }150 return null;151 }152 153 async open(item) {154 if (this.isOpen(item)) { return true; }155 await this.tree.toggleDirectory(item);156 }157 158 async ensureLoaded(item) {159 await item.ensureLoaded();160 }161 162 async ensureVisible(item, align='auto') {163 await this.tree.ensureVisible(item, align);164 }165 166 async openPath(item) {167 parent = item.parent;168 await this.tree.openDirectory(parent);169 }170 171 async close(item) {172 await this.tree.closeDir(item);173 }174 175 async toggle(item) {176 await this.tree.toggleDirectory(item);177 }178 179 async select(item, ensureVisible = false, align = 'auto') {180 await this.tree.setActiveFile(item, ensureVisible, align);181 }182 183 async selectNode(item, ensureVisible = false, align = 'auto') {184 this.tree.setActiveFile(item, ensureVisible, align);185 }186 187 async unload(item) {188 await this.tree.unload(item);189 }190 191 async addIcon(item, icon) {192 if (item?.getMetadata('data') !== undefined) {193 item.getMetadata('data').icon = icon.icon;194 }195 await this.tree.addIcon(item, icon);196 }197 198 removeIcon() {199 // TBD200 }201 202 setLeaf() {203 // TBD204 }205 async setLabel(item, label) {206 if (item) {207 await this.tree.setLabel(item, label);208 }209 }210 211 async setInode(item) {212 if (item._children) item._children = null;213 await this.tree.closeDirectory(item);214 }215 216 async setId(item, data) {217 if (item) {218 item.getMetadata('data').id = data.id;219 }220 }221 222 async deselect(item) {223 await this.tree.deSelectActiveFile(item);224 }225 226 wasInit() {227 // TBD228 return true;229 }230 231 wasLoad(item) {232 if (item?.type === FileType.Directory) {233 return item.isExpanded && item.children != null && item.children.length > 0;234 }235 return true;236 }237 238 parent(item) {239 return item.parent;240 }241 242 first(item) {243 const model = this.tree.getModel();244 if ((item === undefined || item === null) && model.root.children !== null) {245 return model.root.children[0];246 }247 248 if (item?.branchSize > 0) {249 return item.children[0];250 }251 252 return null;253 }254 255 children(item) {256 const model = this.tree.getModel();257 if (item) {258 return (item.children !== null ? item.children : []);259 }260 return model.root.children;261 }262 263 itemFrom(domElem) {264 return this.tree.getItemFromDOM(domElem);265 }266 267 DOMFrom(item) {268 return this.tree.getDOMFromItem(item);269 }270 271 addCssClass(item, cssClass) {272 this.tree.addCssClass(item, cssClass);273 }274 275 path(item) {276 if (item) return item.path;277 }278 279 pathId(item) {280 if (item) {281 let pathIds = item.path.split('/');282 pathIds.splice(0, 1);283 return pathIds;284 }285 return [];286 }287 288 itemFromDOM(domElem) {289 return this.tree.getItemFromDOM(domElem[0]);290 }291 292 siblings(item) {293 if (this.hasParent(item)) {294 let _siblings = this.parent(item).children.filter((_item) => _item.path !== item.path);295 if (typeof (_siblings) !== 'object') return [_siblings];296 else return _siblings;297 }298 return [];299 }300 301 hasParent(item) {302 return item?.parent;303 }304 305 isOpen(item) {306 if (item.type === FileType.Directory) {307 return item.isExpanded;308 }309 return false;310 }311 312 isClosed(item) {313 if (item.type === FileType.Directory) {314 return !item.isExpanded;315 }316 return false;317 }318 319 itemData(item) {320 return (item?.getMetadata('data') !== undefined) ? item?._metadata.data : [];321 }322 323 getData(item) {324 return (item?.getMetadata('data') !== undefined) ? item?._metadata.data : [];325 }326 327 isRootNode(item) {328 const model = this.tree.getModel();329 return item === model.root;330 }331 332 isInode(item) {333 const children = this.children(item);334 if (children === null || children === undefined) return false;335 return children.length > 0;336 }337 338 selected() {339 return this.tree.getActiveFile();340 }341 342 resizeTree() {343 this.tree.resize();344 }345 346 findNodeWithToggle(path) {347 let tree = this;348 349 if (path == null || !Array.isArray(path)) {350 return Promise.reject(new Error(null));351 }352 const basepath = '/browser/' + path.slice(0, path.length-1).join('/') + '/';353 path = '/browser/' + path.join('/');354 355 let onCorrectPath = function (matchPath) {356 return (matchPath !== undefined && path !== undefined357 && (basepath.startsWith(`${matchPath}/`) || path === matchPath));358 };359 360 return (function findInNode(currentNode) {361 return new Promise((resolve, reject) => {362 if (path === null || path === undefined || path.length === 0) {363 resolve(null);364 }365 /* No point in checking the children if366 * the path for currentNode itself is not matching367 */368 if (currentNode.path !== undefined && !onCorrectPath(currentNode.path)) {369 reject(new Error(null));370 } else if (currentNode.path === path) {371 resolve(currentNode);372 } else {373 tree.open(currentNode)374 .then(() => {375 let children = currentNode.children;376 for (let i = 0, length = children.length; i < length; i++) {377 let childNode = children[i];378 if (onCorrectPath(childNode.path)) {379 resolve(findInNode(childNode));380 return;381 }382 }383 reject(new Error(null));384 })385 .catch(() => {386 reject(new Error(null));387 });388 }389 });390 })(tree.tree.getModel().root);391 }392 393 getNodeDisplayPath(item, separator='/', skip_coll=false) {394 let retStack = [];395 let currItem = item;396 while(currItem?.fileName) {397 const data = currItem._metadata?.data;398 if(data._type.startsWith('coll-') && skip_coll) {399 /* Skip collection */400 } else {401 retStack.push(data._label);402 }403 currItem = currItem.parent;404 }405 retStack = retStack.reverse();406 if(!separator) return retStack;407 return retStack.join(separator);408 }409 410 findNodeByDomElement(domElement) {411 const path = domElement?.path;412 if (!path?.[0]) {413 return undefined;414 }415 416 return this.findNode(path);417 }418 419 addNewNode(id, data, item, parentPath) {420 let parent;421 parent = this.findNode(parentPath);422 return this.createOrUpdateNode(id, data, parent, item);423 }424 425 findNode(path) {426 if (path === null || path === undefined || path.length === 0 || path == '/browser') {427 return this.rootNode;428 }429 return findInTree(this.rootNode, path);430 }431 432 createOrUpdateNode(id, data, parent, domNode) {433 let oldNodePath = id;434 if (parent?.path != '/browser') {435 oldNodePath = parent.path + '/' + id;436 }437 const oldNode = this.findNode(oldNodePath);438 if (oldNode !== null) {439 oldNode.data = data;440 oldNode.domNode = domNode;441 return oldNode;442 }443 444 const node = new TreeNode(id, data, domNode, parent);445 if (parent === this.rootNode) {446 node.parentNode = null;447 }448 449 if (parent !== null && parent !== undefined)450 parent.children.push(node);451 return node;452 }453 454 async updateAndReselectNode(item, data) {455 await this.update(item, data);456 await this.deselect(item);457 await this.select(item);458 }459 460 translateTreeNodeIdFromReactTree(treeNode) {461 let currentTreeNode = treeNode;462 let path = [];463 while (currentTreeNode !== null && currentTreeNode !== undefined) {464 if (currentTreeNode.path !== '/browser') path.unshift(currentTreeNode.path);465 if (this.hasParent(currentTreeNode)) {466 currentTreeNode = this.parent(currentTreeNode);467 } else {468 break;469 }470 }471 return path;472 }473 474 getTreeNodeHierarchy(identifier) {475 let idx = 0;476 let node_cnt = 0;477 let result = {};478 if (identifier === undefined) return;479 let item = TreeNode.prototype.isPrototypeOf(identifier) ? identifier : this.findNode(identifier.path);480 if (item === undefined) return;481 do {482 const currentNodeData = item.getData();483 if (currentNodeData._type in this.Nodes && this.Nodes[currentNodeData._type].hasId) {484 const nodeType = mapType(currentNodeData._type, node_cnt);485 if (result[nodeType] === undefined) {486 result[nodeType] = _.extend({}, currentNodeData, {487 'priority': idx,488 });489 idx -= 1;490 }491 }492 node_cnt += 1;493 item = item.hasParent() ? item.parent() : null;494 } while (item);495 496 return result;497 }498 499 /*500 *501 * The dropDetailsFunc should return an object of sample502 * {text: 'xyz', cur: {from:0, to:0} where text is the drop text and503 * cur is selection range of text after dropping. If returned as504 * string, by default cursor will be set to the end of text505 */506 registerDraggableType(typeOrTypeDict, dropDetailsFunc = null) {507 if (typeof typeOrTypeDict == 'object') {508 Object.keys(typeOrTypeDict).forEach((type) => {509 this.registerDraggableType(type, typeOrTypeDict[type]);510 });511 } else if (dropDetailsFunc != null) {512 typeOrTypeDict.replace(/ +/, ' ').split(' ').forEach((type) => {513 this.draggableTypes[type] = dropDetailsFunc;514 });515 }516 }517 518 getDraggable(type) {519 if (this.draggableTypes[type]) {520 return this.draggableTypes[type];521 } else {522 return null;523 }524 }525 526 handleDraggable(e, item) {527 let data = item.getMetadata('data');528 let dropDetailsFunc = this.getDraggable(data._type);529 530 if (dropDetailsFunc != null) {531 let dropDetails = dropDetailsFunc(data, item, this.getTreeNodeHierarchy(item));532 533 if (typeof dropDetails == 'string') {534 dropDetails = {535 text: dropDetails,536 cur: {537 from: dropDetails.length,538 to: dropDetails.length,539 },540 };541 } else if (!dropDetails.cur) {542 dropDetails = {543 ...dropDetails,544 cur: {545 from: dropDetails.text.length,546 to: dropDetails.text.length,547 },548 };549 }550 551 e.dataTransfer.setData('text', JSON.stringify(dropDetails));552 /* Required by Firefox */553 if (e.dataTransfer.dropEffect) {554 e.dataTransfer.dropEffect = 'move';555 }556 557 /* setDragImage is not supported in IE. We leave it to558 * its default look and feel559 */560 const dropText = _.escape(dropDetails.text);561 if(!dropText) {562 e.preventDefault();563 }564 if (e.dataTransfer.setDragImage) {565 const dragItem = document.createElement('div');566 dragItem.classList.add('drag-tree-node');567 dragItem.innerHTML = `<span>${dropText}</span>`;568 569 document.querySelector('body .drag-tree-node')?.remove();570 document.body.appendChild(dragItem);571 572 e.dataTransfer.setDragImage(dragItem, 0, 0);573 }574 }575 else {576 e.preventDefault();577 }578 }579 580 onNodeCopy(copyCallback) {581 this.copyHandler = copyCallback;582 }583}584 585function mapType(type, idx) {586 return (type === 'partition' && idx > 0) ? 'table' : type;587}588 589 590 591/**592 * Given an initial node and a path, it will navigate through593 * the new tree to find the node that matches the path594 */595export function findInTree(rootNode, path) {596 if (path === null) {597 return rootNode;598 }599 return (function findInNode(currentNode) {600 601 /* No point in checking the children if602 * the path for currentNode itself is not matching603 */604 if (currentNode.path !== undefined && path !== undefined605 && !path.startsWith(currentNode.path)) {606 return null;607 }608 609 for (let i = 0, length = currentNode.children.length; i < length; i++) {610 const calculatedNode = findInNode(currentNode.children[i]);611 if (calculatedNode !== null) {612 return calculatedNode;613 }614 }615 616 if (currentNode.path === path) {617 return currentNode;618 } else {619 return null;620 }621 })(rootNode);622}623 624const isValidTreeNodeData = (data) => (!_.isEmpty(data));625 626export { isValidTreeNodeData };627 