From 5c34c10d4e3c56a50e891fd03790832bd9d6bf7d Mon Sep 17 00:00:00 2001 From: Aditya Toshniwal Date: Mon, 2 Jan 2023 10:51:13 +0530 Subject: [PATCH] - Move pgadmin4-treeview to pgAdmin main repo. - Use react based context menu for browser tree. #5615. - Fix feature tests failure. --- web/package.json | 4 - .../browser/static/js/MainMenuFactory.js | 30 +- web/pgadmin/browser/static/js/browser.js | 26 +- web/pgadmin/browser/static/js/keyboard.js | 26 +- .../static/js/components/PreferencesTree.jsx | 8 +- web/pgadmin/static/js/components/Menu.jsx | 8 +- .../components/PgTree/FileTreeItem/index.tsx | 167 +++++ .../js/components/PgTree/FileTreeX/index.tsx | 609 ++++++++++++++++++ .../js/components/PgTree/TreeModelX/index.ts | 10 + .../static/js/components/PgTree/index.ts | 3 + .../js/components/PgTree/scss/styles.scss | 400 ++++++++++++ .../PgTree/services/keyboardHotkeys.ts | 129 ++++ .../static/js/components/PgTree/types.ts | 93 +++ web/pgadmin/static/js/tree/tree.js | 2 +- web/pgadmin/static/js/tree/tree_init.tsx | 194 ++++-- .../static/scss/_pgadmin4-tree.overrides.scss | 186 ------ web/pgadmin/static/scss/pgadmin.scss | 2 - .../feature_tests/keyboard_shortcut_test.py | 13 +- web/regression/feature_utils/pgadmin_page.py | 4 +- .../feature_utils/tree_area_locators.py | 2 +- web/webpack.shim.js | 2 +- web/webpack.test.config.js | 1 - web/yarn.lock | 75 +-- 23 files changed, 1588 insertions(+), 406 deletions(-) create mode 100644 web/pgadmin/static/js/components/PgTree/FileTreeItem/index.tsx create mode 100644 web/pgadmin/static/js/components/PgTree/FileTreeX/index.tsx create mode 100644 web/pgadmin/static/js/components/PgTree/TreeModelX/index.ts create mode 100644 web/pgadmin/static/js/components/PgTree/index.ts create mode 100644 web/pgadmin/static/js/components/PgTree/scss/styles.scss create mode 100644 web/pgadmin/static/js/components/PgTree/services/keyboardHotkeys.ts create mode 100644 web/pgadmin/static/js/components/PgTree/types.ts delete mode 100644 web/pgadmin/static/scss/_pgadmin4-tree.overrides.scss diff --git a/web/package.json b/web/package.json index cd30686bf..c0d0f9413 100644 --- a/web/package.json +++ b/web/package.json @@ -112,7 +112,6 @@ "classnames": "^2.2.6", "closest": "^0.0.1", "codemirror": "^5.59.2", - "context-menu": "^2.0.0", "convert-units": "^2.3.4", "cssnano": "^5.0.2", "dagre": "^0.8.4", @@ -123,8 +122,6 @@ "insert-if": "^1.1.0", "ip-address": "^7.1.0", "jquery": "^3.6.0", - "jquery-contextmenu": "^2.9.2", - "jquery-ui": "^1.13.2", "json-bignumber": "^1.0.1", "jsoneditor": "^9.5.4", "jsoneditor-react": "^3.1.1", @@ -140,7 +137,6 @@ "path-fx": "^2.0.0", "pathfinding": "^0.4.18", "paths-js": "^0.4.9", - "pgadmin4-tree": "git+https://github.com/EnterpriseDB/pgadmin4-treeview/#96ceb7f27f43660a804e61d23a76aeb9aa188bb6", "postcss": "^8.2.15", "raf": "^3.4.1", "rc-dock": "^3.2.9", diff --git a/web/pgadmin/browser/static/js/MainMenuFactory.js b/web/pgadmin/browser/static/js/MainMenuFactory.js index 6573c9445..700326149 100644 --- a/web/pgadmin/browser/static/js/MainMenuFactory.js +++ b/web/pgadmin/browser/static/js/MainMenuFactory.js @@ -80,34 +80,8 @@ export default class MainMenuFactory { }); } - static getContextMenu(menuList, item, node) { + static getContextMenu(menuList) { Menu.sortMenus(menuList); - - let ctxMenus = {}; - let ctxIndex = 1; - menuList.forEach(ctx => { - let ctx_uid = _.uniqueId('ctx_'); - let sub_ctx_item = {}; - ctx.checkAndSetDisabled(node, item); - if (ctx.getMenuItems()) { - // Menu.sortMenus(ctx.getMenuItems()); - ctx.getMenuItems().forEach((c) => { - c.checkAndSetDisabled(node, item); - if (!c.isDisabled) { - sub_ctx_item[ctx_uid + _.uniqueId('_sub_')] = c.getContextItem(c.label, c.isDisabled); - } - }); - } - if (!ctx.isDisabled) { - if(ctx.type == 'separator') { - ctxMenus[ctx_uid + '_' + ctx.priority + '_' + + ctxIndex + '_sep'] = '----'; - } else { - ctxMenus[ctx_uid + '_' + ctx.priority + '_' + + ctxIndex + '_itm'] = ctx.getContextItem(ctx.label, ctx.isDisabled, sub_ctx_item); - } - } - ctxIndex++; - }); - - return ctxMenus; + return menuList; } } diff --git a/web/pgadmin/browser/static/js/browser.js b/web/pgadmin/browser/static/js/browser.js index 25c3c6ee7..cf54dfbcb 100644 --- a/web/pgadmin/browser/static/js/browser.js +++ b/web/pgadmin/browser/static/js/browser.js @@ -468,7 +468,7 @@ define('pgadmin.browser', [ let menuItemList = obj.getMenuList('object', item, d); objectMenu && MainMenuFactory.refreshMainMenuItems(objectMenu, menuItemList); let ctxMenuList = obj.getMenuList('context', item, d, true); - obj.BrowserContextMenu = MainMenuFactory.getContextMenu(ctxMenuList, item, d); + obj.BrowserContextMenu = MainMenuFactory.getContextMenu(ctxMenuList); } else { objectMenu && MainMenuFactory.refreshMainMenuItems(objectMenu, [ MainMenuFactory.createMenuItem({ @@ -564,30 +564,6 @@ define('pgadmin.browser', [ obj?.editor?.refresh(); }, 10); - // Build the treeview context menu - $('#tree').contextMenu({ - selector: '.file-entry', - autoHide: false, - build: function(element) { - let item = obj.tree.itemFrom(element), - context_menu = {}; - - if(item) obj.tree.select(item); - context_menu = obj.BrowserContextMenu; - - return { - autoHide: false, - items: context_menu, - }; - }, - events: { - hide: function() { - // Return focus to the tree - obj.keyboardNavigation.bindLeftTree(); - }, - }, - }); - // Register scripts and add menus pgBrowser.utils.registerScripts(this); diff --git a/web/pgadmin/browser/static/js/keyboard.js b/web/pgadmin/browser/static/js/keyboard.js index 5ef23bb8d..95497a0f4 100644 --- a/web/pgadmin/browser/static/js/keyboard.js +++ b/web/pgadmin/browser/static/js/keyboard.js @@ -15,6 +15,7 @@ import * as commonUtils from '../../../static/js/utils'; import dialogTabNavigator from '../../../static/js/dialog_tab_navigator'; import * as keyboardFunc from 'sources/keyboard_shortcuts'; import pgWindow from 'sources/window'; +import gettext from 'sources/gettext'; const pgBrowser = pgAdmin.Browser = pgAdmin.Browser || {}; @@ -130,10 +131,27 @@ _.extend(pgBrowser.keyboardNavigation, { }, bindMainMenu: function(event, combo) { const shortcut_obj = this.keyboardShortcut; - if (combo === shortcut_obj.file_shortcut) $('#mnu_file a.dropdown-toggle').dropdown('toggle'); - if (combo === shortcut_obj.object_shortcut) $('#mnu_obj a.dropdown-toggle').first().dropdown('toggle'); - if (combo === shortcut_obj.tools_shortcut) $('#mnu_tools a.dropdown-toggle').dropdown('toggle'); - if (combo === shortcut_obj.help_shortcut) $('#mnu_help a.dropdown-toggle').dropdown('toggle'); + let menuLabel = null; + switch (combo) { + case shortcut_obj.file_shortcut: + menuLabel = gettext('File'); + break; + case shortcut_obj.object_shortcut: + menuLabel = gettext('Object'); + break; + case shortcut_obj.tools_shortcut: + menuLabel = gettext('Tools'); + break; + case shortcut_obj.help_shortcut: + menuLabel = gettext('Help'); + break; + default: + break; + } + + if(menuLabel) { + document.querySelector(`#main-menu-container button[data-label="${menuLabel}"]`)?.click(); + } }, bindRightPanel: function(event, combo) { let allPanels = pgAdmin.Browser.docker.findPanels(); diff --git a/web/pgadmin/preferences/static/js/components/PreferencesTree.jsx b/web/pgadmin/preferences/static/js/components/PreferencesTree.jsx index a640a45b9..0c08ba645 100644 --- a/web/pgadmin/preferences/static/js/components/PreferencesTree.jsx +++ b/web/pgadmin/preferences/static/js/components/PreferencesTree.jsx @@ -11,10 +11,10 @@ import gettext from 'sources/gettext'; import * as React from 'react'; import PropTypes from 'prop-types'; import { Directory} from 'react-aspen'; -import { FileTreeX, TreeModelX } from 'pgadmin4-tree'; import { Tree } from '../../../../static/js/tree/tree'; import { ManagePreferenceTreeNodes } from '../../../../static/js/tree/preference_nodes'; import pgAdmin from 'sources/pgadmin'; +import { FileTreeX, TreeModelX } from '../../../../static/js/components/PgTree'; export default function PreferencesTree({ pgBrowser, data }) { @@ -36,7 +36,7 @@ export default function PreferencesTree({ pgBrowser, data }) { pathStyle: 'unix', getItems: (path) => { return ptree.readNode(path); - + }, sortComparator: (a, b) => { // No nee to sort Query tool options. @@ -63,7 +63,7 @@ export default function PreferencesTree({ pgBrowser, data }) { pTreeModelX.current.root._children.forEach((_d)=> { _d.root.expandDirectory(_d); }); - + return true; }; @@ -82,4 +82,4 @@ PreferencesTree.propTypes = { pgBrowser: PropTypes.any, data: PropTypes.array, ptree: PropTypes.any, -}; \ No newline at end of file +}; diff --git a/web/pgadmin/static/js/components/Menu.jsx b/web/pgadmin/static/js/components/Menu.jsx index c6308cba4..cada75d1b 100644 --- a/web/pgadmin/static/js/components/Menu.jsx +++ b/web/pgadmin/static/js/components/Menu.jsx @@ -30,7 +30,10 @@ const useStyles = makeStyles((theme)=>({ }, '& .szh-menu__item': { display: 'flex', - padding: '4px 8px', + padding: '4px 12px', + '&:after': { + right: '0.75rem', + }, '&.szh-menu__item--active, &.szh-menu__item--hover': { backgroundColor: theme.palette.primary.main, color: theme.palette.primary.contrastText, @@ -50,7 +53,7 @@ const useStyles = makeStyles((theme)=>({ } })); -export function PgMenu({open, className, label, menuButton, ...props}) { +export function PgMenu({open, className='', label, menuButton=null, ...props}) { const classes = useStyles(); const state = open ? 'open' : 'closed'; props.anchorRef?.current?.setAttribute('data-state', state); @@ -67,6 +70,7 @@ export function PgMenu({open, className, label, menuButton, ...props}) { )`. + * Where `` can be either `NewFilePromptHandle.parent` or `RenamePromptHandle.target` depending on type of `PromptHandle` + * + * To determine the type of `PromptHandle`, use `IItemRendererProps.itemType` + */ + decorations: ClasslistComposite + onClick: (ev: React.MouseEvent, item: FileEntry | Directory, type: ItemType) => void + onContextMenu: (ev: React.MouseEvent, item: FileEntry | Directory) => void +} + +// DO NOT EXTEND FROM PureComponent!!! You might miss critical changes made deep within `item` prop +// as far as efficiency is concerned, `react-aspen` works hard to ensure unnecessary updates are ignored +export class FileTreeItem extends React.Component { + public static getBoundingClientRectForItem(item: FileEntry | Directory): ClientRect { + const divRef = FileTreeItem.itemIdToRefMap.get(item.id) + if (divRef) { + return divRef.getBoundingClientRect() + } + return null + } + + // ensure this syncs up with what goes in CSS, (em, px, % etc.) and what ultimately renders on the page + public static readonly renderHeight: number = 24 + private static readonly itemIdToRefMap: Map = new Map() + private static readonly refToItemIdMap: Map = new Map() + private fileTreeEvent: IFileTreeXTriggerEvents + + constructor(props) { + super(props) + // used to apply decoration changes, you're welcome to use setState or other mechanisms as you see fit + this.forceUpdate = this.forceUpdate.bind(this) + } + + public render() { + const { item, itemType, decorations } = this.props + + const isRenamePrompt = itemType === ItemType.RenamePrompt + const isNewPrompt = itemType === ItemType.NewDirectoryPrompt || itemType === ItemType.NewFilePrompt + const isPrompt = isRenamePrompt || isNewPrompt + const isDirExpanded = itemType === ItemType.Directory + ? (item as Directory).expanded + : itemType === ItemType.RenamePrompt && (item as RenamePromptHandle).target.type === FileType.Directory + ? ((item as RenamePromptHandle).target as Directory).expanded + : false + + const fileOrDir = + (itemType === ItemType.File || + itemType === ItemType.NewFilePrompt || + (itemType === ItemType.RenamePrompt && (item as RenamePromptHandle).target.constructor === FileEntry)) + ? 'file' + : 'directory' + + if (this.props.item.parent && this.props.item.parent.path) { + this.props.item.resolvedPathCache = this.props.item.parent.path + "/" + this.props.item._metadata.data.id + } + + const itemChildren = item.children && item.children.length > 0 && item._metadata.data._type.indexOf('coll-') !== -1 ? "(" + item.children.length + ")" : "" + const is_root = this.props.item.parent.path === '/browser' + const extraClasses = item._metadata.data.extraClasses ? item._metadata.data.extraClasses.join(' ') : '' + + return ( +
+ + {!isNewPrompt && fileOrDir === 'directory' ? + + : null + } + + + { + item._metadata && item._metadata.data.icon ? + : null + } + + { _.unescape(this.props.item.getMetadata('data')._label)} + {itemChildren} + + + +
) + } + + public componentDidMount() { + this.events = this.props.events + this.props.item.resolvedPathCache = this.props.item.parent.path + "/" + this.props.item._metadata.data.id + if (this.props.decorations) { + this.props.decorations.addChangeListener(this.forceUpdate) + } + this.setActiveFile(this.props.item) + } + + private setActiveFile = async (FileOrDir): Promise => { + this.props.changeDirectoryCount(FileOrDir.parent) + if(FileOrDir._loaded !== true) { + this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'added', FileOrDir) + } + FileOrDir._loaded = true + } + + public componentWillUnmount() { + if (this.props.decorations) { + this.props.decorations.removeChangeListener(this.forceUpdate) + } + } + + public componentDidUpdate(prevProps: IItemRendererXProps) { + if (prevProps.decorations) { + prevProps.decorations.removeChangeListener(this.forceUpdate) + } + if (this.props.decorations) { + this.props.decorations.addChangeListener(this.forceUpdate) + } + } + + private handleDivRef = (r: HTMLDivElement) => { + if (r === null) { + FileTreeItem.itemIdToRefMap.delete(this.props.item.id) + } else { + FileTreeItem.itemIdToRefMap.set(this.props.item.id, r) + FileTreeItem.refToItemIdMap.set(r, this.props.item) + } + } + + private handleContextMenu = (ev: React.MouseEvent) => { + const { item, itemType, onContextMenu } = this.props + if (itemType === ItemType.File || itemType === ItemType.Directory) { + onContextMenu(ev, item as FileOrDir) + } + } + + private handleClick = (ev: React.MouseEvent) => { + const { item, itemType, onClick } = this.props + if (itemType === ItemType.File || itemType === ItemType.Directory) { + onClick(ev, item as FileEntry, itemType) + } + } + + private handleDoubleClick = (ev: React.MouseEvent) => { + const { item, itemType, onDoubleClick } = this.props + if (itemType === ItemType.File || itemType === ItemType.Directory) { + onDoubleClick(ev, item as FileEntry, itemType) + } + } +} diff --git a/web/pgadmin/static/js/components/PgTree/FileTreeX/index.tsx b/web/pgadmin/static/js/components/PgTree/FileTreeX/index.tsx new file mode 100644 index 000000000..578b2e112 --- /dev/null +++ b/web/pgadmin/static/js/components/PgTree/FileTreeX/index.tsx @@ -0,0 +1,609 @@ +import * as React from 'react' +import { + FileTree, + Directory, + FileEntry, + ItemType, + IFileTreeHandle, + WatchEvent, + FileType, + IItemRendererProps, + FileOrDir +} from 'react-aspen' +import { Decoration, TargetMatchMode } from 'aspen-decorations' +import { FileTreeItem } from '../FileTreeItem' +import { Notificar, DisposablesComposite } from 'notificar' +import { IFileTreeXHandle, IFileTreeXProps, FileTreeXEvent, IFileTreeXTriggerEvents } from '../types' +import { KeyboardHotkeys } from '../services/keyboardHotkeys' +import { TreeModelX } from '../TreeModelX' +import AutoSizer from "react-virtualized-auto-sizer"; + +export class FileTreeX extends React.Component { + private fileTreeHandle: IFileTreeXHandle + private activeFileDec: Decoration + private pseudoActiveFileDec: Decoration + private activeFile: FileOrDir + private pseudoActiveFile: FileOrDir + private wrapperRef: React.RefObject = React.createRef() + private events: Notificar + private disposables: DisposablesComposite + private keyboardHotkeys: KeyboardHotkeys + private fileTreeEvent: IFileTreeXTriggerEvents + constructor(props: IFileTreeXProps) { + super(props) + this.events = new Notificar() + this.disposables = new DisposablesComposite() + this.activeFileDec = new Decoration('active') + this.pseudoActiveFileDec = new Decoration('pseudo-active') + } + + render() { + const { height, width, model, disableCache } = this.props + const { decorations } = model + + return
+ + {({ width, height }) => ( + + {(props: IItemRendererProps) => } + + )} + +
+ } + + public componentDidMount() { + for(let child of this.props.model.root.children) { + this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'loaded', child) + } + } + + componentWillUnmount() { + const { model } = this.props + model.decorations.removeDecoration(this.activeFileDec) + model.decorations.removeDecoration(this.pseudoActiveFileDec) + this.disposables.dispose() + } + + private handleTreeEvent = (event: IFileTreeXTriggerEvents) => { + this.fileTreeEvent = this.props.onEvent + } + + private handleTreeReady = (handle: IFileTreeHandle) => { + const { onReady, model } = this.props + + this.fileTreeHandle = { + ...handle, + getModel: () => this.props.model, + getActiveFile: () => this.activeFile, + setActiveFile: this.setActiveFile, + getPseudoActiveFile: () => this.pseudoActiveFile, + setPseudoActiveFile: this.setPseudoActiveFile, + toggleDirectory: this.toggleDirectory, + closeDir: this.closeDir, + remove: this.removeDir, + newFile: async (dirOrPath: Directory | string) => this.supervisePrompt(await handle.promptNewFile(dirOrPath as any)), + newFolder: async (dirOrPath: Directory | string) => this.supervisePrompt(await handle.promptNewDirectory(dirOrPath as any)), + onBlur: (callback) => this.events.add(FileTreeXEvent.OnBlur, callback), + hasDirectFocus: () => this.wrapperRef.current === document.activeElement, + first: this.first, + parent: this.parent, + hasParent: this.hasParent, + isOpen: this.isOpen, + isClosed: this.isClosed, + itemData: this.itemData, + children: this.children, + getItemFromDOM: this.getItemFromDOM, + getDOMFromItem: this.getDOMFromItem, + onTreeEvents: (callback) => this.events.add(FileTreeXEvent.onTreeEvents, callback), + addIcon: this.addIcon, + addCssClass: this.addCssClass, + create: this.create, + remove: this.remove, + update: this.update, + refresh: this.refresh, + setLabel: this.setLabel, + unload: this.unload, + deSelectActiveFile: this.deSelectActiveFile, + resize: this.resize, + showLoader: this.showLoader, + hideLoader: this.hideLoader, + } + + model.decorations.addDecoration(this.activeFileDec) + model.decorations.addDecoration(this.pseudoActiveFileDec) + + this.disposables.add(this.fileTreeHandle.onDidChangeModel((prevModel: TreeModelX, newModel: TreeModelX) => { + this.setActiveFile(null) + this.setPseudoActiveFile(null) + prevModel.decorations.removeDecoration(this.activeFileDec) + prevModel.decorations.removeDecoration(this.pseudoActiveFileDec) + newModel.decorations.addDecoration(this.activeFileDec) + newModel.decorations.addDecoration(this.pseudoActiveFileDec) + })) + + this.disposables.add(this.fileTreeHandle.onBlur(() => { + this.setPseudoActiveFile(null) + })) + + this.keyboardHotkeys = new KeyboardHotkeys(this.fileTreeHandle) + + if (typeof onReady === 'function') { + onReady(this.fileTreeHandle) + } + } + + private setActiveFile = async (fileOrDirOrPath: FileOrDir | string, ensureVisible, align): Promise => { + const fileH = typeof fileOrDirOrPath === 'string' + ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath) + : fileOrDirOrPath + + if (fileH === this.props.model.root) { return } + if (this.activeFile !== fileH) { + if (this.activeFile) { + this.activeFileDec.removeTarget(this.activeFile) + } + if (fileH) { + this.activeFileDec.addTarget(fileH as any, TargetMatchMode.Self) + } + this.activeFile = fileH + this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'selected', fileH) + + if (fileH && ensureVisible === true) { + const alignTree = align !== undefined && align !== null ? align : 'auto' + await this.fileTreeHandle.ensureVisible(fileH, alignTree) + } + } + } + + private ensureVisible = async (fileOrDirOrPath: FileOrDir | string): Promise => { + const fileH = typeof fileOrDirOrPath === 'string' + ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath) + : fileOrDirOrPath + + if (fileH) { + await this.fileTreeHandle.ensureVisible(fileH) + } + } + + private deSelectActiveFile = async (fileOrDirOrPath: FileOrDir | string): Promise => { + const fileH = typeof fileOrDirOrPath === 'string' + ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath) + : fileOrDirOrPath + + if (fileH === this.props.model.root) { return } + if (this.activeFile === fileH) { + this.activeFileDec.removeTarget(this.activeFile) + this.activeFile = null + } + } + + private setPseudoActiveFile = async (fileOrDirOrPath: FileOrDir | string): Promise => { + const fileH = typeof fileOrDirOrPath === 'string' + ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath) + : fileOrDirOrPath + + if (fileH === this.props.model.root) { return } + if (this.pseudoActiveFile !== fileH) { + if (this.pseudoActiveFile) { + this.pseudoActiveFileDec.removeTarget(this.pseudoActiveFile) + } + if (fileH) { + this.pseudoActiveFileDec.addTarget(fileH as any, TargetMatchMode.Self) + } + this.pseudoActiveFile = fileH + } + if (fileH) { + await this.fileTreeHandle.ensureVisible(fileH) + } + this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'selected', fileH) + } + + private create = async (parentDir, itemData): Promise => { + if (parentDir == undefined || parentDir == null) { + parentDir = this.props.model.root + } + const {create, model } = this.props + const isOpen = parentDir.isExpanded + let maybeFile = undefined + + if (isOpen && (parentDir._children == null || parentDir._children.length == 0)) { + await this.fileTreeHandle.closeDirectory(parentDir as Directory) + } + if (!parentDir.isExpanded && (parentDir._children == null || parentDir._children.length == 0)) { + await this.fileTreeHandle.openDirectory(parentDir as Directory) + } else { + await this.fileTreeHandle.openDirectory(parentDir as Directory) + maybeFile = await create(parentDir.path, itemData) + if (maybeFile && maybeFile.type && maybeFile.name) { + model.root.inotify({ + type: WatchEvent.Added, + directory: parentDir.path, + file: maybeFile, + }) + } + } + this.changeDirectoryCount(parentDir) + let newItem = parentDir._children.find((c) => c._metadata.data.id === itemData.id) + newItem.resolvedPathCache = newItem.parent.path + "/" + newItem._metadata.data.id + return newItem + } + + private update = async (item, itemData): Promise => { + item._metadata.data = itemData + await this.props.update(item.path, itemData) + this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'updated', item) + } + + private refresh = async (item): Promise => { + const {remove, model } = this.props + const isOpen = item.isExpanded + if (item.children && item.children.length > 0) { + for(let entry of item.children) { + await this.remove(entry).then(val => {}, error => {console.warn("Error removing item")}) + } + } + if (isOpen) { + const ref = FileTreeItem.itemIdToRefMap.get(item.id); + + if (ref) { + this.showLoader(ref) + } + + await this.fileTreeHandle.closeDirectory(item as Directory) + await this.fileTreeHandle.openDirectory(item as Directory) + await this.changeResolvePath(item as Directory) + this.changeDirectoryCount(item) + + if (ref) { + this.hideLoader(ref) + } + } + } + + private unload = async (item): Promise => { + const {remove, model } = this.props + const isOpen = item.isExpanded + if (item.children && item.children.length > 0) { + for(let entry of item.children) { + await this.remove(entry).then(val => {}, error => {console.warn(error)}) + } + } + if (isOpen) { + await this.fileTreeHandle.closeDirectory(item as Directory) + this.changeDirectoryCount(item) + } + } + + private remove = async (item): Promise => { + const {remove, model } = this.props + const path = item.path + await remove(path, false) + const dirName = model.root.pathfx.dirname(path); + const fileName = model.root.pathfx.basename(path); + const parent = item.parent + if (dirName === parent.path) { + const item_1 = parent._children.find((c) => c._metadata && c._metadata.data.id === fileName); + if (item_1) { + parent.unlinkItem(item_1); + if (parent._children.length == 0) { parent._children = null } + this.changeDirectoryCount(parent) + this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'removed', item) + } + else { + console.warn("Item not found") + } + } + } + + private first = async (fileOrDirOrPath: FileOrDir | string) => { + const fileH = typeof fileOrDirOrPath === 'string' + ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath) + : fileOrDirOrPath + + if (fileH === undefined || fileH === null) { return this.props.model.root.children[0] } + + if (fileH.branchSize > 0) { + return fileH.children[0] + } + return null + } + + private parent = async (fileOrDirOrPath: FileOrDir | string) => { + const fileH = typeof fileOrDirOrPath === 'string' + ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath) + : fileOrDirOrPath + + if (fileH === FileType.Directory || fileH === FileType.File) { + return fileH.parent + } + + return null + } + + + private hasParent = async (fileOrDirOrPath: FileOrDir | string) => { + const fileH = typeof fileOrDirOrPath === 'string' + ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath) + : fileOrDirOrPath + + if (fileH === FileType.Directory || fileH === FileType.File) { + return fileH.parent ? true : false + } + + return false + } + + private children = async (fileOrDirOrPath: FileOrDir | string) => { + const fileH = typeof fileOrDirOrPath === 'string' + ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath) + : fileOrDirOrPath + + if (fileH === FileType.Directory) { + return fileH.children + } + + return null + } + + + private isOpen = async (fileOrDirOrPath: FileOrDir | string) => { + const fileH = typeof fileOrDirOrPath === 'string' + ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath) + : fileOrDirOrPath + + if (fileH === FileType.Directory) { + return fileH.isExpanded + } + + return false + } + + private isClosed = async (fileOrDirOrPath: FileOrDir | string) => { + const fileH = typeof fileOrDirOrPath === 'string' + ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath) + : fileOrDirOrPath + + if (fileH === FileType.Directory || fileH === FileType.File) { + return !fileH.isExpanded + } + + return false + } + + private itemData = async (fileOrDirOrPath: FileOrDir | string) => { + const fileH = typeof fileOrDirOrPath === 'string' + ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath) + : fileOrDirOrPath + + if (fileH === FileType.Directory || fileH === FileType.File) { + return fileH._metadata.data + } + + return null + } + + private setLabel = async(pathOrDir: string | Directory, label: string): Promise => { + const dir = typeof pathOrDir === 'string' + ? await this.fileTreeHandle.getFileHandle(pathOrDir) + : pathOrDir + + const ref = FileTreeItem.itemIdToRefMap.get(dir.id); + if (ref) { + ref.style.background = 'none' + const label$ = ref.querySelector('span.file-name') as HTMLDivElement + + if (label$) { + if (typeof(label) == "object" && label.label) { + label = label.label + } + label$.innerHTML = label; + } + + } + + } + + private changeDirectoryCount = async(pathOrDir: string | Directory): Promise => { + const dir = typeof pathOrDir === 'string' + ? await this.fileTreeHandle.getFileHandle(pathOrDir) + : pathOrDir + + if (dir.type === FileType.Directory && dir._metadata.data && dir._metadata.data.is_collection === true) { + const ref = FileTreeItem.itemIdToRefMap.get(dir.id); + if (ref) { + ref.style.background = 'none' + const label$ = ref.querySelector('span.children-count') as HTMLDivElement + if(dir.children && dir.children.length > 0) { + label$.innerHTML = "(" + dir.children.length + ")"; + } else { + label$.innerHTML = ""; + } + } + } + + } + + private closeDir = async (pathOrDir: string | Directory) => { + const dir = typeof pathOrDir === 'string' + ? await this.fileTreeHandle.getFileHandle(pathOrDir) + : pathOrDir + + if (dir.type === FileType.Directory) { + if ((dir as Directory).expanded) { + this.fileTreeHandle.closeDirectory(dir as Directory) + this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'closed', dir) + + } + } + } + + private toggleDirectory = async (pathOrDir: string | Directory) => { + const dir = typeof pathOrDir === 'string' + ? await this.fileTreeHandle.getFileHandle(pathOrDir) + : pathOrDir + + if (dir.type === FileType.Directory) { + if ((dir as Directory).expanded) { + this.fileTreeHandle.closeDirectory(dir as Directory) + this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'closed', dir) + + } else { + const ref = FileTreeItem.itemIdToRefMap.get(dir.id); + if (ref) { + this.showLoader(ref) + } + + await this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'beforeopen', dir) + await this.fileTreeHandle.openDirectory(dir as Directory) + await this.changeResolvePath(dir as Directory) + + if (ref) { + this.hideLoader(ref) + } + + this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'opened', dir) + } + } + } + + private addIcon = async (pathOrDir: string | Directory, icon) => { + const dir = typeof pathOrDir === 'string' + ? await this.fileTreeHandle.getFileHandle(pathOrDir) + : pathOrDir + + const ref = FileTreeItem.itemIdToRefMap.get(dir.id); + if (ref) { + const label$ = ref.querySelector('.file-label i') as HTMLDivElement + label$.className = icon.icon; + } + + } + + private addCssClass = async (pathOrDir: string | Directory, cssClass) => { + const dir = typeof pathOrDir === 'string' + ? await this.fileTreeHandle.getFileHandle(pathOrDir) + : pathOrDir + + const ref = FileTreeItem.itemIdToRefMap.get(dir.id); + if (ref) { + ref.classList.add(cssClass) + if (!dir._metadata.data.extraClasses) + dir._metadata.data.extraClasses = [] + + dir._metadata.data.extraClasses.push(cssClass) + } + + } + + private showLoader = (ref: HTMLDivElement) => { + // get label ref and add loading class + ref.style.background = 'none' + const label$ = ref.querySelector('i.directory-toggle') as HTMLDivElement + if (label$) label$.classList.add("loading") + } + + private hideLoader = (ref: HTMLDivElement) => { + // remove loading class. + ref.style.background = 'none' + const label$ = ref.querySelector('i.directory-toggle') as HTMLDivElement + if (label$) label$.classList.remove("loading") + } + + private handleBlur = () => { + this.events.dispatch(FileTreeXEvent.OnBlur) + } + + private handleItemClicked = async (ev: React.MouseEvent, item: FileOrDir, type: ItemType) => { + if (type === ItemType.Directory && ev.target.className.includes("directory-toggle")) { + await this.toggleDirectory(item as Directory) + } + await this.setActiveFile(item as FileEntry) + + } + + private handleItemDoubleClicked = async (ev: React.MouseEvent, item: FileOrDir, type: ItemType) => { + await this.toggleDirectory(item as Directory) + await this.setActiveFile(item as FileEntry) + + } + + private getItemFromDOM = (clientReact) => { + return FileTreeItem.refToItemIdMap.get(clientReact); + } + + private getDOMFromItem = (item: FileOrDir) => { + return FileTreeItem.itemIdToRefMap.get(item.id); + } + + private handleClick = (ev: React.MouseEvent) => { + // clicked in "blank space" + if (ev.currentTarget === ev.target) { + this.setPseudoActiveFile(null) + } + } + + private handleItemCtxMenu = (ev: React.MouseEvent, item: FileOrDir) => { + return this.props.onContextMenu?.(ev, item); + } + + private handleKeyDown = (ev: React.KeyboardEvent) => { + return this.keyboardHotkeys.handleKeyDown(ev) + } + + private onResize = (...args) => { + if (this.wrapperRef.current != null) { + this.resize() + } + } + + private resize = (scrollX, scrollY) => { + const scrollXPos = scrollX ? scrollX : 0 + const scrollYPos = scrollY ? scrollY : this.props.model.state.scrollOffset + const div = this.wrapperRef.current.querySelector('div').querySelector('div') as HTMLDivElement + div.scroll(scrollXPos, scrollYPos) + + } + + private changeResolvePath = async (item: FileOrDir): Promise => { + // Change the path as per pgAdmin requirement: Item Id wise + if (item.type === FileType.File) { + item.resolvedPathCache = item.parent.path + "/" + item._metadata.data.id + } + if (item.type === FileType.Directory && item.children && item.children.length > 0) { + for(let entry of item.children) { + entry.resolvedPathCache = entry.parent.path + "/" + entry._metadata.data.id + } + } + } +} + +export { IFileTreeXHandle, IFileTreeXProps } diff --git a/web/pgadmin/static/js/components/PgTree/TreeModelX/index.ts b/web/pgadmin/static/js/components/PgTree/TreeModelX/index.ts new file mode 100644 index 000000000..8c3d2a0f9 --- /dev/null +++ b/web/pgadmin/static/js/components/PgTree/TreeModelX/index.ts @@ -0,0 +1,10 @@ +import { TreeModel, IBasicFileSystemHost } from 'react-aspen' +import { DecorationsManager } from 'aspen-decorations' + +export class TreeModelX extends TreeModel { + public readonly decorations: DecorationsManager + constructor(host: IBasicFileSystemHost, mountPath: string) { + super(host, mountPath) + this.decorations = new DecorationsManager(this.root as any) + } +} diff --git a/web/pgadmin/static/js/components/PgTree/index.ts b/web/pgadmin/static/js/components/PgTree/index.ts new file mode 100644 index 000000000..bde582afd --- /dev/null +++ b/web/pgadmin/static/js/components/PgTree/index.ts @@ -0,0 +1,3 @@ +export { FileTreeX } from './FileTreeX' +export { TreeModelX } from './TreeModelX' +export { IFileTreeXHandle, IFileTreeXProps, FileTreeXEvent, IFileTreeXTriggerEvents } from './types' diff --git a/web/pgadmin/static/js/components/PgTree/scss/styles.scss b/web/pgadmin/static/js/components/PgTree/scss/styles.scss new file mode 100644 index 000000000..f6d2cb92d --- /dev/null +++ b/web/pgadmin/static/js/components/PgTree/scss/styles.scss @@ -0,0 +1,400 @@ +.file-tree { + font-family: $font-family-primary !important; + font-size: $tree-font-size !important; + background-color: $color-bg !important; + display: inline-block; + color: $tree-text-fg !important; + + &, + & * { + box-sizing: border-box; + } + + width: 100%; +} + +.browser-tree { + height: 100%; +} + +.file-tree> { + div { + position: absolute !important; + height: 100% !important; + top: 0px !important; + + >div { + scrollbar-gutter: stable; + overflow: overlay !important; + } + } +} + +.file-entry { + font: inherit; + text-align: left; + display: flex; + align-items: center; + white-space: nowrap; + padding: 2px 0; + cursor: pointer !important; + color: $tree-text-fg !important; + + &:before { + content: ''; + background: $color-gray-light; + position: absolute; + width: 1px; + height: 100%; + // set box-shadow to show tree indent guide. + box-shadow: -16px 0 0 0 $color-gray-light, + -32px 0 0 0 $color-gray-light, + -48px 0 0 0 $color-gray-light, + -64px 0 0 0 $color-gray-light, + -80px 0 0 0 $color-gray-light, + -96px 0 0 0 $color-gray-light, + -112px 0 0 0 $color-gray-light, + -128px 0 0 0 $color-gray-light, + -144px 0 0 0 $color-gray-light, + -160px 0 0 0 $color-gray-light, + -176px 0 0 0 $color-gray-light, + -192px 0 0 0 $color-gray-light, + -208px 0 0 0 $color-gray-light, + -224px 0 0 0 $color-gray-light, + -240px 0 0 0 $color-gray-light, + -256px 0 0 0 $color-gray-light, + -272px 0 0 0 $color-gray-light, + -288px 0 0 0 $color-gray-light, + -304px 0 0 0 $color-gray-light, + -320px 0 0 0 $color-gray-light, + -336px 0 0 0 $color-gray-light, + -352px 0 0 0 $color-gray-light, + -368px 0 0 0 $color-gray-light, + -384px 0 0 0 $color-gray-light, + -400px 0 0 0 $color-gray-light, + -416px 0 0 0 $color-gray-light, + -432px 0 0 0 $color-gray-light, + -448px 0 0 0 $color-gray-light, + -464px 0 0 0 $color-gray-light, + -480px 0 0 0 $color-gray-light, + -496px 0 0 0 $color-gray-light, + -512px 0 0 0 $color-gray-light, + -528px 0 0 0 $color-gray-light, + -544px 0 0 0 $color-gray-light, + -544px 0 0 0 $color-gray-light, + -560px 0 0 0 $color-gray-light; + } + + &:hover, + &.pseudo-active { + background-color: $tree-bg-hover !important; + color: $tree-fg-hover !important; + + span.file-label { + span.file-name { + color: $tree-text-hover-fg !important; + } + } + } + + &.active, + &.prompt { + background-color: $tree-bg-selected !important; + border-color: $color-primary-light; + border-right: $active-border !important; + color: $tree-text-hover-fg !important; + + span.file-label { + span.file-name { + color: $tree-text-hover-fg !important; + } + } + } + + i { + display: inline-block; + font-size: 18px; + text-align: center; + height: 21px !important; + width: 20px !important; + flex-shrink: 0; + + &:before { + height: inherit; + width: inherit; + display: inline-block; + } + + &.directory-toggle { + &:before { + background-position: 6px center !important; + font-family: $font-family-icon; + content: "\f054" !important; + border-style: none; + margin-left: 5px; + font-weight: 900; + right: 15px; + top: 3px; + font-size: 0.6rem; + line-height: 2; + } + + &.open:before { + background-position: -14px center !important; + font-family: $font-family-icon; + content: "\f078" !important; + border-style: none; + margin-left: 5px; + font-weight: 900; + transform: none !important; + } + + &.loading:before { + content: '' !important; + font-family: $font-family-icon; + border-style: none; + background: $loader-icon-small 0 0 no-repeat; + background-position: center !important; + } + } + } + + span.file-label { + display: flex; + align-items: center; + padding: 0 2px 0 2px; + border: 1px solid transparent; + height: auto; + white-space: normal; + cursor: pointer !important; + margin-left: 2px; + + &:hover, + &.pseudo-active { + color: $tree-fg-hover !important; + } + } + + &.prompt.new .file-label, + &.file .file-label { + margin-left: 18px; + } + + span.file-name { + font: inherit; + flex-grow: 1; + user-select: none; + color: $tree-text-fg !important; + margin-left: 3px; + cursor: pointer !important; + white-space: nowrap; + + &:hover, + &.pseudo-active { + color: $tree-fg-hover !important; + } + } +} + +// Set the tree depth CSS from depth level 21 to 50 as in default CSS depth is set from 0 to 20. +@for $i from 21 through 50 { + .file-entry.depth-#{$i} { + padding-left: 16px * ($i - 1); + } +} + +.children-count { + margin-left: 3px; +} + +::-webkit-scrollbar { + width: 8px; + height: 8px; + + &-track { + background: transparent; + } + + &-corner { + background: transparent; + } + + &-thumb { + background: #3a3a3a; + + &:hover { + background: #424242; + } + } +} + +.file-tree { + font-family: 'Segoe UI'; + font-size: 15px; + color: #c1c1c1; + background-color: #1f1f1f; + display: inline-block; + + &, + & * { + box-sizing: border-box; + } +} + +.file-entry { + font: inherit; + text-align: left; + display: flex; + align-items: center; + white-space: nowrap; + padding: 2px 0; + padding-left: 2px; + cursor: default; + + &.red { + filter: saturate(.5); + + .file-icon:after { + content: ''; + height: 8px; + width: 8px; + font-weight: bold; + display: inline-block; + background: #da2d38; + position: relative; + border-radius: 4px; + left: -7px; + box-sizing: border-box; + } + } + + &.magenta span.file-name { + color: magenta; + } + + &.big { + font-family: monospace; + } + + &:hover, + &.pseudo-active { + background-color: #2d2d2d; + } + + &.active, + &.prompt { + background-color: #333333; + } + + &.dragging { + background: #2a2a2a; + } + + &.dragover { + background-color: #313131; + } + + i { + display: inline-block; + font: normal normal normal 18px/1 "default-icons"; + font-size: 18px; + text-align: center; + height: 18px; + width: 18px; + + &:before { + height: inherit; + width: inherit; + display: inline-block; + } + } + + span.file-label { + display: flex; + align-items: center; + } + + &.prompt.new .file-label, + &.file .file-label { + margin-left: 18px; + } + + span.file-name { + font: inherit; + flex-grow: 1; + user-select: none; + cursor: default; + color: #c1c1c1; + margin-left: 3px; + + & input[type='text'] { + display: block; + width: 94%; + margin: 0; + font: inherit; + border-radius: 3px; + padding: 1px 2px; + border: 0; + background: #2d2d2d; + color: inherit; + outline: none; + position: relative; + z-index: 1; + margin-top: -2px; + top: 1px; + left: -2px; + + &:focus { + box-shadow: 0px 0px 1px 1px #1ead7f; + } + + &.invalid { + box-shadow: 0px 0px 1px 1px #a51c15; + + &+span.prompt-err-msg { + position: relative; + display: block; + + &:after { + content: 'Invalid filename'; + position: absolute; + background: rgb(105, 30, 30); + width: 94%; + padding: 1px 2px; + box-sizing: border-box; + border-radius: 0px 0px 4px 4px; + box-shadow: 0px 0px 0px 1px #a51c15; + font-size: 14px; + z-index: 2; + } + } + } + + &.invalid-input-pulse { + &+span.prompt-err-msg { + animation: pulsate-err-msg .3s ease-in-out 2; + } + } + } + } +} + +@for $i from 2 through 20 { + .file-entry.depth-#{$i} { + padding-left: 16px * ($i - 1); + } +} + +@keyframes pulsate-err-msg { + 0% { + color: #868686; + } + + 50% { + color: inherit; + } + + 100% { + color: #868686; + } +} diff --git a/web/pgadmin/static/js/components/PgTree/services/keyboardHotkeys.ts b/web/pgadmin/static/js/components/PgTree/services/keyboardHotkeys.ts new file mode 100644 index 000000000..9c7b09db8 --- /dev/null +++ b/web/pgadmin/static/js/components/PgTree/services/keyboardHotkeys.ts @@ -0,0 +1,129 @@ +import { FileEntry, Directory, FileType } from 'react-aspen' +import { IFileTreeXHandle } from '../types' + +export class KeyboardHotkeys { + private hotkeyActions = { + 'ArrowUp': () => this.jumpToPrevItem(), + 'ArrowDown': () => this.jumpToNextItem(), + 'ArrowRight': () => this.expandOrJumpToFirstChild(), + 'ArrowLeft': () => this.collapseOrJumpToFirstParent(), + 'Space': () => this.toggleDirectoryExpand(), + 'Enter': () => this.selectFileOrToggleDirState(), + 'Home': () => this.jumpToFirstItem(), + 'End': () => this.jumpToLastItem(), + 'Escape': () => this.resetSteppedOrSelectedItem(), + } + + constructor(private readonly fileTreeX: IFileTreeXHandle) { } + + public handleKeyDown = (ev: React.KeyboardEvent) => { + if (!this.fileTreeX.hasDirectFocus()) { + return false + } + const { code } = ev.nativeEvent + if (code in this.hotkeyActions) { + ev.preventDefault() + this.hotkeyActions[code]() + return true + } + } + + private jumpToFirstItem = (): void => { + const { root } = this.fileTreeX.getModel() + this.fileTreeX.setActiveFile(root.getFileEntryAtIndex(0), true) + } + + private jumpToLastItem = (): void => { + const { root } = this.fileTreeX.getModel() + this.fileTreeX.setActiveFile(root.getFileEntryAtIndex(root.branchSize - 1), true) + } + + private jumpToNextItem = (): void => { + const { root } = this.fileTreeX.getModel() + let currentPseudoActive = this.fileTreeX.getActiveFile() + if (!currentPseudoActive) { + const selectedFile = this.fileTreeX.getActiveFile() + if (selectedFile) { + currentPseudoActive = selectedFile + } else { + return this.jumpToFirstItem() + } + } + const idx = root.getIndexAtFileEntry(currentPseudoActive) + if (idx + 1 > root.branchSize) { + return this.jumpToFirstItem() + } else if (idx > -1) { + this.fileTreeX.setActiveFile(root.getFileEntryAtIndex(idx + 1), true) + } + } + + private jumpToPrevItem = (): void => { + const { root } = this.fileTreeX.getModel() + let currentPseudoActive = this.fileTreeX.getActiveFile() + if (!currentPseudoActive) { + const selectedFile = this.fileTreeX.getActiveFile() + if (selectedFile) { + currentPseudoActive = selectedFile + } else { + return this.jumpToLastItem() + } + } + const idx = root.getIndexAtFileEntry(currentPseudoActive) + if (idx - 1 < 0) { + return this.jumpToLastItem() + } else if (idx > -1) { + this.fileTreeX.setActiveFile(root.getFileEntryAtIndex(idx - 1), true) + } + } + + private expandOrJumpToFirstChild(): void { + const currentPseudoActive = this.fileTreeX.getActiveFile() + if (currentPseudoActive && currentPseudoActive.type === FileType.Directory) { + if ((currentPseudoActive as Directory).expanded) { + return this.jumpToNextItem() + } else { + this.fileTreeX.openDirectory(currentPseudoActive as Directory) + } + } + } + + private collapseOrJumpToFirstParent(): void { + const currentPseudoActive = this.fileTreeX.getActiveFile() + if (currentPseudoActive) { + if (currentPseudoActive.type === FileType.Directory && (currentPseudoActive as Directory).expanded) { + return this.fileTreeX.closeDirectory(currentPseudoActive as Directory) + } + this.fileTreeX.setActiveFile(currentPseudoActive.parent, true) + } + } + + private selectFileOrToggleDirState = (): void => { + const currentPseudoActive = this.fileTreeX.getActiveFile() + if (!currentPseudoActive) { return } + if (currentPseudoActive.type === FileType.Directory) { + this.fileTreeX.toggleDirectory(currentPseudoActive as Directory) + } else if (currentPseudoActive.type === FileType.File) { + this.fileTreeX.setActiveFile(currentPseudoActive as FileEntry, true) + } + } + + private toggleDirectoryExpand = (): void => { + const currentPseudoActive = this.fileTreeX.getActiveFile() + if (!currentPseudoActive) { return } + if (currentPseudoActive.type === FileType.Directory) { + this.fileTreeX.toggleDirectory(currentPseudoActive as Directory) + } + } + + private resetSteppedOrSelectedItem = (): void => { + const currentPseudoActive = this.fileTreeX.getActiveFile() + if (currentPseudoActive) { + return this.resetSteppedItem() + } + this.fileTreeX.setActiveFile(null) + } + + private resetSteppedItem = () => { + this.fileTreeX.setActiveFile(null) + } +} diff --git a/web/pgadmin/static/js/components/PgTree/types.ts b/web/pgadmin/static/js/components/PgTree/types.ts new file mode 100644 index 000000000..b3d97dd3a --- /dev/null +++ b/web/pgadmin/static/js/components/PgTree/types.ts @@ -0,0 +1,93 @@ +import { IFileTreeHandle, FileEntry, Directory, TreeModel, FileType, IFileEntryItem, IItemRenderer, FileOrDir } from 'react-aspen' +import { IDisposable } from 'notificar' +import { TreeModelX } from './TreeModelX' +import React, { MouseEventHandler } from 'react' +import { MenuItem } from '../../helpers/Menu' + + +export interface IFileTreeXTriggerEvents { + onEvent(event: string, path: string): boolean | Promise +} + +export interface IItemRendererX extends IItemRenderer { + getBoundingClientRectForItem(item: FileEntry | Directory): ClientRect +} + +// Here imagination is your limit! IFileTreeHandle has core low-level features you can build on top of as your application needs +export interface IFileTreeXHandle extends IFileTreeHandle { + getActiveFile(): FileEntry | Directory + setActiveFile(path: string) + setActiveFile(file: FileEntry) + setActiveFile(dir: Directory) + + getPseudoActiveFile(): FileEntry | Directory + setPseudoActiveFile(path: string) + setPseudoActiveFile(file: FileEntry) + setPseudoActiveFile(dir: Directory) + + rename(path: string) + rename(file: FileEntry) + rename(dir: Directory) + + newFile(dirpath: string) + newFile(dir: Directory) + newFolder(dirpath: string) + newFolder(dir: Directory) + toggleDirectory(path: string) + toggleDirectory(dir: Directory) + + first(file: FileEntry): FileEntry | Directory + first(dir: Directory): FileEntry | Directory + first(): FileEntry | Directory + + parent(file: FileEntry): Directory + parent(dir: Directory): Directory + + hasParent(file: FileEntry): boolean + hasParent(dir: Directory): boolean + + isOpen(file: FileEntry): boolean + isOpen(dir: Directory): boolean + + isClosed(file: FileEntry): boolean + isClosed(dir: Directory): boolean + + itemData(file: FileEntry): array + itemData(dir: Directory): array + + children(file: FileEntry): array + children(dir: Directory): array + + getModel(): TreeModelX + /** + * If document.activeElement === filetree wrapper element + */ + hasDirectFocus(): boolean + + // events + onBlur(callback: () => void): IDisposable +} + +export interface IFileTreeXProps { + height: number + width: number + model: TreeModelX + + /** + * Same as unix's `mv` command as in `mv [SOURCE] [DEST]` + */ + mv: (oldPath: string, newPath: string) => boolean | Promise + + /** + * Amalgam of unix's `mkdir` and `touch` command + */ + create: (path: string, type: FileType) => IFileEntryItem | Promise + onReady?: (handle: IFileTreeXHandle) => void + onEvent?: (event: IFileTreeXTriggerEvents) => void + onContextMenu?: (ev: React.MouseEvent, item?: FileOrDir) => void +} + +export enum FileTreeXEvent { + OnBlur, + onTreeEvents, +} diff --git a/web/pgadmin/static/js/tree/tree.js b/web/pgadmin/static/js/tree/tree.js index dad785b1b..b9790332f 100644 --- a/web/pgadmin/static/js/tree/tree.js +++ b/web/pgadmin/static/js/tree/tree.js @@ -253,7 +253,7 @@ export class Tree { } itemFrom(domElem) { - return this.tree.getItemFromDOM(domElem[0]); + return this.tree.getItemFromDOM(domElem); } DOMFrom(item) { diff --git a/web/pgadmin/static/js/tree/tree_init.tsx b/web/pgadmin/static/js/tree/tree_init.tsx index 2716e17a8..4bccdedcc 100644 --- a/web/pgadmin/static/js/tree/tree_init.tsx +++ b/web/pgadmin/static/js/tree/tree_init.tsx @@ -8,91 +8,153 @@ ////////////////////////////////////////////////////////////// import * as React from 'react'; -import { render } from 'react-dom'; -import { FileTreeX, TreeModelX } from 'pgadmin4-tree'; +import ReactDOM from 'react-dom'; import {Tree} from './tree'; -import { IBasicFileSystemHost, Directory } from 'react-aspen'; +import { IBasicFileSystemHost, Directory, FileOrDir } from 'react-aspen'; import { ManageTreeNodes } from './tree_nodes'; import pgAdmin from 'sources/pgadmin'; +import { FileTreeX, TreeModelX } from '../components/PgTree'; +import Theme from '../Theme'; +import { PgMenu, PgMenuDivider, PgMenuItem, PgSubMenu } from '../components/Menu'; -var initBrowserTree = async (pgBrowser) => { - const MOUNT_POINT = '/browser' +var initBrowserTree = (pgBrowser) => { + return new Promise((resolve, reject)=>{ + const MOUNT_POINT = '/browser' - // Setup host - let mtree = new ManageTreeNodes(); + // Setup host + let mtree = new ManageTreeNodes(); - // Init Tree with the Tree Parent node '/browser' - mtree.init(MOUNT_POINT); + // Init Tree with the Tree Parent node '/browser' + mtree.init(MOUNT_POINT); - const host: IBasicFileSystemHost = { - pathStyle: 'unix', - getItems: async (path) => { - return mtree.readNode(path); - }, - sortComparator: (a: FileEntry | Directory, b: FileEntry | Directory) => { - // No nee to sort columns - if (a._metadata && a._metadata.data._type == 'column') return 0; - // Sort alphabetically - if (a.constructor === b.constructor) { - return pgAdmin.natural_sort(a.fileName, b.fileName); + const host: IBasicFileSystemHost = { + pathStyle: 'unix', + getItems: async (path) => { + return mtree.readNode(path); + }, + sortComparator: (a: FileEntry | Directory, b: FileEntry | Directory) => { + // No nee to sort columns + if (a._metadata && a._metadata.data._type == 'column') return 0; + // Sort alphabetically + if (a.constructor === b.constructor) { + return pgAdmin.natural_sort(a.fileName, b.fileName); + } + let retval = 0; + if (a.constructor === Directory) { + retval = -1; + } else if (b.constructor === Directory) { + retval = 1; + } + return retval; + }, + } + + // Create Node + const create = async (parentPath, _data): Promise => { + try { + let _node_path = parentPath + "/" + _data.id + return mtree.addNode(parentPath, _node_path, _data) + } catch (error) { + return null // or throw error as you see fit } - let retval = 0; - if (a.constructor === Directory) { - retval = -1; - } else if (b.constructor === Directory) { - retval = 1; + } + + // Remove Node + const remove = async (path: string, _removeOnlyChild): Promise => { + try { + await mtree.removeNode(path, _removeOnlyChild); + return true + } catch (error) { + return false // or throw error as you see fit } - return retval; - }, - } - - // Create Node - const create = async (parentPath, _data): Promise => { - try { - let _node_path = parentPath + "/" + _data.id - return mtree.addNode(parentPath, _node_path, _data) - } catch (error) { - return null // or throw error as you see fit } - } - - // Remove Node - const remove = async (path: string, _removeOnlyChild): Promise => { - try { - await mtree.removeNode(path, _removeOnlyChild); - return true - } catch (error) { - return false // or throw error as you see fit + // Update Node + const update = async (path: string, data): Promise => { + try { + await mtree.updateNode(path, data); + return true + } catch (error) { + return false // or throw error as you see fit + } } - } - // Update Node - const update = async (path: string, data): Promise => { - try { - await mtree.updateNode(path, data); - return true - } catch (error) { - return false // or throw error as you see fit + const treeModelX = new TreeModelX(host, MOUNT_POINT) + + const itemHandle = function onReady(handler) { + // Initialize pgBrowser Tree + pgBrowser.tree = new Tree(handler, mtree, pgBrowser); + resolve(pgBrowser); } - } - const treeModelX = new TreeModelX(host, MOUNT_POINT) + treeModelX.root.ensureLoaded().then(()=>{ + // Render Browser Tree + ReactDOM.render( + + , document.getElementById('tree') + ); + }); + }); +} - const itemHandle = function onReady(handler) { - // Initialize pgBrowser Tree - pgBrowser.tree = new Tree(handler, mtree, pgBrowser); - return true; - } +function BrowserTree(props) { + const [contextPos, setContextPos] = React.useState<{x: number, y: number} | null>(null); + const contextMenuItems = pgAdmin.Browser.BrowserContextMenu; - await treeModelX.root.ensureLoaded(); + const getPgMenuItem = (menuItem, i)=>{ + if(menuItem.type == 'separator') { + return ; + } + const hasCheck = typeof menuItem.checked == 'boolean'; - // Render Browser Tree - await render( - - , document.getElementById('tree')); + return { + menuItem.callback(); + }} + hasCheck={hasCheck} + checked={menuItem.checked} + >{menuItem.label}; + }; + + const onContextMenu = React.useCallback(async (ev: MouseEvent, item: FileOrDir)=>{ + ev.preventDefault(); + if(item) { + await pgAdmin.Browser.tree.select(item); + setContextPos({x: ev.clientX, y: ev.clientY}); + } + }, []); + + return ( + + + setContextPos(null)} + label="context" + > + {contextMenuItems.length !=0 && contextMenuItems.map((menuItem, i)=>{ + const submenus = menuItem.getMenuItems(); + if(submenus) { + return + {submenus.map((submenuItem, si)=>{ + return getPgMenuItem(submenuItem, si); + })} + ; + } + return getPgMenuItem(menuItem, i); + })} + + + ) } module.exports = { diff --git a/web/pgadmin/static/scss/_pgadmin4-tree.overrides.scss b/web/pgadmin/static/scss/_pgadmin4-tree.overrides.scss deleted file mode 100644 index 640a45073..000000000 --- a/web/pgadmin/static/scss/_pgadmin4-tree.overrides.scss +++ /dev/null @@ -1,186 +0,0 @@ -.file-tree { - font-family: $font-family-primary !important; - font-size: $tree-font-size !important; - background-color: $color-bg !important; - display: inline-block; - color: $tree-text-fg !important; - &, & * { - box-sizing: border-box; - } - width: 100%; -} - -.browser-tree { - height: 100%; -} - -.file-tree > { - div { - position: absolute !important; - height: 100% !important; - top: 0px !important; - - > div { - scrollbar-gutter: stable; - overflow: overlay !important; - } - } -} - -.file-entry -{ - font: inherit; - text-align: left; - display: flex; - align-items: center; - white-space: nowrap; - padding: 2px 0; - cursor: pointer !important; - color: $tree-text-fg !important; - &:before { - content: ''; - background: $color-gray-light; - position: absolute; - width: 1px; - height: 100%; - // set box-shadow to show tree indent guide. - box-shadow: -16px 0 0 0 $color-gray-light, - -32px 0 0 0 $color-gray-light, - -48px 0 0 0 $color-gray-light, - -64px 0 0 0 $color-gray-light, - -80px 0 0 0 $color-gray-light, - -96px 0 0 0 $color-gray-light, - -112px 0 0 0 $color-gray-light, - -128px 0 0 0 $color-gray-light, - -144px 0 0 0 $color-gray-light, - -160px 0 0 0 $color-gray-light, - -176px 0 0 0 $color-gray-light, - -192px 0 0 0 $color-gray-light, - -208px 0 0 0 $color-gray-light, - -224px 0 0 0 $color-gray-light, - -240px 0 0 0 $color-gray-light, - -256px 0 0 0 $color-gray-light, - -272px 0 0 0 $color-gray-light, - -288px 0 0 0 $color-gray-light, - -304px 0 0 0 $color-gray-light, - -320px 0 0 0 $color-gray-light, - -336px 0 0 0 $color-gray-light, - -352px 0 0 0 $color-gray-light, - -368px 0 0 0 $color-gray-light, - -384px 0 0 0 $color-gray-light, - -400px 0 0 0 $color-gray-light, - -416px 0 0 0 $color-gray-light, - -432px 0 0 0 $color-gray-light, - -448px 0 0 0 $color-gray-light, - -464px 0 0 0 $color-gray-light, - -480px 0 0 0 $color-gray-light, - -496px 0 0 0 $color-gray-light, - -512px 0 0 0 $color-gray-light, - -528px 0 0 0 $color-gray-light, - -544px 0 0 0 $color-gray-light, - -544px 0 0 0 $color-gray-light, - -560px 0 0 0 $color-gray-light; - } - &:hover, &.pseudo-active { - background-color: $tree-bg-hover !important; - color: $tree-fg-hover !important; - span.file-label { - span.file-name { - color: $tree-text-hover-fg !important; - } - } - } - &.active, &.prompt { - background-color: $tree-bg-selected !important; - border-color: $color-primary-light; - border-right: $active-border !important; - color: $tree-text-hover-fg !important; - span.file-label { - span.file-name { - color: $tree-text-hover-fg !important; - } - } - } - i { - display: inline-block; - font-size: 18px; - text-align: center; - height: 21px !important; - width: 20px !important; - flex-shrink: 0; - &:before { - height: inherit; - width: inherit; - display: inline-block; - } - &.directory-toggle { - &:before { - background-position: 6px center !important; - font-family: $font-family-icon; - content: "\f054" !important; - border-style: none; - margin-left: 5px; - font-weight: 900; - right: 15px; - top: 3px; - font-size: 0.6rem; - line-height: 2; - } - &.open:before { - background-position: -14px center !important; - font-family: $font-family-icon; - content: "\f078" !important; - border-style: none; - margin-left: 5px; - font-weight: 900; - transform: none !important; - } - &.loading:before { - content: '' !important; - font-family: $font-family-icon; - border-style: none; - background: $loader-icon-small 0 0 no-repeat; - background-position: center !important; - } - } - } - span.file-label { - display: flex; - align-items: center; - padding:0 2px 0 2px; - border:1px solid transparent; - height:auto; - white-space:normal; - cursor:pointer !important; - margin-left: 2px; - &:hover, &.pseudo-active { - color: $tree-fg-hover !important; - } - } - &.prompt.new .file-label, &.file .file-label { - margin-left: 18px; - } - span.file-name { - font: inherit; - flex-grow: 1; - user-select: none; - color: $tree-text-fg !important; - margin-left: 3px; - cursor: pointer !important; - white-space: nowrap; - &:hover, &.pseudo-active { - color: $tree-fg-hover !important; - } - } -} - -// Set the tree depth CSS from depth level 21 to 50 as in default CSS depth is set from 0 to 20. -@for $i from 21 through 50 { - .file-entry.depth-#{$i} { - padding-left: 16px * ($i - 1); - } -} - -.children-count { - margin-left: 3px; -} diff --git a/web/pgadmin/static/scss/pgadmin.scss b/web/pgadmin/static/scss/pgadmin.scss index 2416b6782..5e5aa1e50 100644 --- a/web/pgadmin/static/scss/pgadmin.scss +++ b/web/pgadmin/static/scss/pgadmin.scss @@ -28,7 +28,5 @@ $theme-colors: ( @import 'pgadmin.style'; @import 'bootstrap4-toggle.overrides'; @import 'jsoneditor.overrides'; -@import 'pgadmin4-tree.overrides'; -@import 'pgadmin4-tree/src/css/styles'; @import 'rc-dock/dist/rc-dock.css'; @import '@szhsin/react-menu/dist/index.css'; diff --git a/web/regression/feature_tests/keyboard_shortcut_test.py b/web/regression/feature_tests/keyboard_shortcut_test.py index ce90f48e3..f5dd6b8b3 100644 --- a/web/regression/feature_tests/keyboard_shortcut_test.py +++ b/web/regression/feature_tests/keyboard_shortcut_test.py @@ -31,11 +31,11 @@ class KeyboardShortcutFeatureTest(BaseFeatureTest): def before(self): self.new_shortcuts = { - 'mnu_file': { + 'File': { 'shortcut': [Keys.ALT, Keys.SHIFT, 'i'], 'locator': 'File main menu' }, - 'mnu_obj': { + 'Object': { 'shortcut': [Keys.ALT, Keys.SHIFT, 'j'], 'locator': 'Object main menu' } @@ -72,15 +72,12 @@ class KeyboardShortcutFeatureTest(BaseFeatureTest): self.wait.until( EC.presence_of_element_located( - (By.XPATH, "//li[contains(@id, " + - s + - ") and contains(@class, 'show')]") + (By.CSS_SELECTOR, + "[role='menu'][aria-label='File'].szh-menu--state-open") ) ) - is_open = 'show' in self.page.find_by_id(s).get_attribute('class') - - assert is_open is True, "Keyboard shortcut change is unsuccessful." + assert True, "Keyboard shortcut change is unsuccessful." print("OK", file=sys.stderr) diff --git a/web/regression/feature_utils/pgadmin_page.py b/web/regression/feature_utils/pgadmin_page.py index 72605a343..af69752c1 100644 --- a/web/regression/feature_utils/pgadmin_page.py +++ b/web/regression/feature_utils/pgadmin_page.py @@ -98,9 +98,9 @@ class PgadminPage: server_group_node = \ self.find_by_xpath(TreeAreaLocators.server_group_node("Servers")) ActionChains(self.driver).context_click(server_group_node).perform() - ActionChains(self.driver).move_to_element(self.find_by_xpath( + ActionChains(self.driver).move_to_element(self.find_by_css_selector( TreeAreaLocators.context_menu_element('Register'))).perform() - ActionChains(self.driver).move_to_element(self.find_by_xpath( + ActionChains(self.driver).move_to_element(self.find_by_css_selector( TreeAreaLocators.context_menu_element('Server...'))) \ .click().perform() diff --git a/web/regression/feature_utils/tree_area_locators.py b/web/regression/feature_utils/tree_area_locators.py index 462f9170d..a3f8ef438 100644 --- a/web/regression/feature_utils/tree_area_locators.py +++ b/web/regression/feature_utils/tree_area_locators.py @@ -159,7 +159,7 @@ class TreeAreaLocators: # Context element option @staticmethod def context_menu_element(schema_name): - return "//li/span[text()='%s']" % schema_name + return "[role='menuitem'][data-label='%s']" % schema_name # Old xpaths # server_group_sub_nodes_exp_status = \ diff --git a/web/webpack.shim.js b/web/webpack.shim.js index 27a99284b..0e24c9e4a 100644 --- a/web/webpack.shim.js +++ b/web/webpack.shim.js @@ -102,7 +102,6 @@ let webpackShimConfig = { 'react-dom': path.join(__dirname, 'node_modules/react-dom'), 'stylis': path.join(__dirname, 'node_modules/stylis'), 'popper.js': path.join(__dirname, 'node_modules/popper.js'), - 'pgadmin4-tree': path.join(__dirname, 'node_modules/pgadmin4-tree'), //xterm 'xterm': path.join(__dirname, './node_modules/xterm/lib/xterm.js'), @@ -283,6 +282,7 @@ let webpackShimConfig = { /* These will be included in array formed by recursive traversing for css/scss files */ css_bundle_include: [ + './pgadmin/static/js/components/PgTree/scss/styles.scss', './pgadmin/static/scss/pgadmin.scss', './pgadmin/static/css/pgadmin.css', ], diff --git a/web/webpack.test.config.js b/web/webpack.test.config.js index 3a9d14693..162c7b739 100644 --- a/web/webpack.test.config.js +++ b/web/webpack.test.config.js @@ -139,7 +139,6 @@ module.exports = { 'browser': path.resolve(__dirname, 'pgadmin/browser/static/js'), 'pgadmin': sourcesDir + '/js/pgadmin', 'pgadmin.sqlfoldcode': sourcesDir + '/js/codemirror/addon/fold/pgadmin-sqlfoldcode', - 'pgadmin4-tree': path.join(__dirname, 'node_modules/pgadmin4-tree'), 'pgbrowser': path.resolve(__dirname, 'regression/javascript/fake_browser'), 'pgadmin.schema.dir': path.resolve(__dirname, 'pgadmin/browser/server_groups/servers/databases/schemas/static/js'), 'pgadmin.browser.layout': path.join(__dirname, './pgadmin/browser/static/js/layout'), diff --git a/web/yarn.lock b/web/yarn.lock index 463e68254..b21a7842f 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -2340,7 +2340,7 @@ aspen-core@^1.0.4: p-series "^1.1.0" path-fx "^2.1.1" -aspen-decorations@^1.0.2, aspen-decorations@^1.1.1: +aspen-decorations@^1.0.2: version "1.1.1" resolved "https://registry.yarnpkg.com/aspen-decorations/-/aspen-decorations-1.1.1.tgz#7d0ca740efab1aa4fd91a1f3db81ac29186607a3" integrity sha512-Ej2tv0Gz3bnhkNCyzzjDeG2V5vd49T30ca0SKywHuLA5RKrZ1NutEyZnUYku4WmUV1/TdpHRiSJ759nbZK4xtQ== @@ -3385,13 +3385,6 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== -context-menu@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/context-menu/-/context-menu-2.0.0.tgz#565f13210248e3442700e6b1a2d63406f2b08552" - integrity sha512-VQrkvcJDevuq+sde0QADRLOdIRpa4a1ti4knstrPILDLfWU/RB4ZIGpj32Chh/mURjrbi0CoLT1eonr3X86Khg== - dependencies: - tiny-emitter "^2.0.2" - convert-source-map@^1.5.0, convert-source-map@^1.7.0, convert-source-map@^1.8.0: version "1.9.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" @@ -6133,21 +6126,14 @@ jmespath@^0.16.0: resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.16.0.tgz#b15b0a85dfd4d930d43e69ed605943c802785076" integrity sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw== -jquery-contextmenu@^2.6.4, jquery-contextmenu@^2.9.2: +jquery-contextmenu@^2.6.4: version "2.9.2" resolved "https://registry.yarnpkg.com/jquery-contextmenu/-/jquery-contextmenu-2.9.2.tgz#f9dc362e45871dda2e50fa45de2243e917446ced" integrity sha512-6S6sH/08owDStC/7zNwcN366yR0ydX6PmMB0RnjLRQOp7Nc/rqwEHglshfHrrw2kdTev97GXwRXrayDUmToIOw== dependencies: jquery "^3.5.0" -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 <4.0.0", jquery@^3.3.1, jquery@^3.5.0, jquery@^3.6.0: +"jquery@>=1.7.1 <4.0.0", jquery@^3.3.1, jquery@^3.5.0, jquery@^3.6.0: version "3.6.1" resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.1.tgz#fab0408f8b45fc19f956205773b62b292c147a16" integrity sha512-opJeO4nCucVnsjiXOE+/PcCgYw9Gwpvs/a6B1LL/lQhwWwpbVEVYDZ1FokFr8PRc7ghYlrFPuyHuiiDNTQxmcw== @@ -7831,27 +7817,6 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== -"pgadmin4-tree@git+https://github.com/EnterpriseDB/pgadmin4-treeview/#96ceb7f27f43660a804e61d23a76aeb9aa188bb6": - version "1.0.0" - resolved "git+https://github.com/EnterpriseDB/pgadmin4-treeview/#96ceb7f27f43660a804e61d23a76aeb9aa188bb6" - dependencies: - "@types/classnames" "^2.2.6" - "@types/react" "^16.7.18" - "@types/react-dom" "^16.0.11" - aspen-decorations "^1.1.1" - browserfs "^1.4.3" - classnames "^2.2.6" - context-menu "^2.0.0" - insert-if "^1.1.0" - lodash "4.*" - notificar "^1.0.1" - path-fx "^2.0.0" - react "^16.6.3" - react-aspen "^1.2.0" - react-dom "^16.6.3" - react-virtualized-auto-sizer "^1.0.6" - valid-filename "^2.0.1" - picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" @@ -8531,7 +8496,7 @@ re-resizable@6.9.6: dependencies: fast-memoize "^2.5.1" -react-aspen@^1.1.0, react-aspen@^1.2.0: +react-aspen@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/react-aspen/-/react-aspen-1.2.0.tgz#375fa82a8db627542fc8b9e6e421baa49a65ab95" integrity sha512-w+vUn4ScCzcxDB5xEsKIuIkUnySEQXlp/zqPFChWEpYG12mPO7h7z/LWuK2QXUoDbIP96Fcf1+UAI9I/cstPqg== @@ -8576,16 +8541,6 @@ react-dnd@^16.0.1: fast-deep-equal "^3.1.3" hoist-non-react-statics "^3.3.2" -react-dom@^16.6.3: - version "16.14.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89" - integrity sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - scheduler "^0.19.1" - react-dom@^17.0.1: version "17.0.2" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" @@ -8747,15 +8702,6 @@ react-window@^1.3.1, react-window@^1.8.5: "@babel/runtime" "^7.0.0" memoize-one ">=3.1.1 <6" -react@^16.6.3: - version "16.14.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" - integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - react@^17.0.1: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" @@ -9117,14 +9063,6 @@ sax@^1.2.4: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== -scheduler@^0.19.1: - version "0.19.1" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196" - integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - scheduler@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" @@ -9997,11 +9935,6 @@ timers-browserify@^1.0.1: dependencies: process "~0.11.0" -tiny-emitter@^2.0.2: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" - integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== - tiny-warning@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"