- Move pgadmin4-treeview to pgAdmin main repo.

- Use react based context menu for browser tree. #5615.
- Fix feature tests failure.
pull/5696/head
Aditya Toshniwal 2023-01-02 10:51:13 +05:30 committed by GitHub
parent 64af035ce9
commit 5c34c10d4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1588 additions and 406 deletions

View File

@ -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",

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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();

View File

@ -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,
};
};

View File

@ -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}) {
<ControlledMenu
state={state}
{...props}
portal
className={clsx(classes.menu, className)}
aria-label={label || 'Menu'}
data-state={state}

View File

@ -0,0 +1,167 @@
import cn from 'classnames'
import * as React from 'react'
import { ClasslistComposite } from 'aspen-decorations'
import { Directory, FileEntry, IItemRendererProps, ItemType, PromptHandle, RenamePromptHandle, FileType, FileOrDir} from 'react-aspen'
import {IFileTreeXTriggerEvents, FileTreeXEvent } from '../types'
import _ from 'lodash'
interface IItemRendererXProps {
/**
* In this implementation, decoration are null when item is `PromptHandle`
*
* If you would like decorations for `PromptHandle`s, then get them using `DecorationManager#getDecorations(<target>)`.
* Where `<target>` 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<IItemRendererXProps & IItemRendererProps> {
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<number, HTMLDivElement> = new Map()
private static readonly refToItemIdMap: Map<number, HTMLDivElement> = 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 (
<div
className={cn('file-entry', {
renaming: isRenamePrompt,
prompt: isRenamePrompt || isNewPrompt,
new: isNewPrompt,
}, fileOrDir, decorations ? decorations.classlist : null, `depth-${item.depth}`, extraClasses)}
data-depth={item.depth}
onContextMenu={this.handleContextMenu}
onClick={this.handleClick}
onDoubleClick={this.handleDoubleClick}
// required for rendering context menus when opened through context menu button on keyboard
ref={this.handleDivRef}
draggable={false}>
{!isNewPrompt && fileOrDir === 'directory' ?
<i className={cn('directory-toggle', isDirExpanded ? 'open' : '')} />
: null
}
<span className='file-label'>
{
item._metadata && item._metadata.data.icon ?
<i className={cn('file-icon', item._metadata && item._metadata.data.icon ? item._metadata.data.icon : fileOrDir)} /> : null
}
<span className='file-name'>
{ _.unescape(this.props.item.getMetadata('data')._label)}
<span className='children-count'>{itemChildren}</span>
</span>
</span>
</div>)
}
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<void> => {
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)
}
}
}

View File

@ -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<IFileTreeXProps> {
private fileTreeHandle: IFileTreeXHandle
private activeFileDec: Decoration
private pseudoActiveFileDec: Decoration
private activeFile: FileOrDir
private pseudoActiveFile: FileOrDir
private wrapperRef: React.RefObject<HTMLDivElement> = React.createRef()
private events: Notificar<FileTreeXEvent>
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 <div
onKeyDown={this.handleKeyDown}
className='file-tree'
onBlur={this.handleBlur}
onClick={this.handleClick}
ref={this.wrapperRef}
style={{
height: height ? height : "calc(100vh - 60px)",
width: "100%",
display: "flex",
flexDirection: "column",
flex: 1
}}
tabIndex={-1}>
<AutoSizer onResize={this.onResize}>
{({ width, height }) => (
<FileTree
height={height}
width={width}
model={model}
itemHeight={FileTreeItem.renderHeight}
onReady={this.handleTreeReady}
disableCache={disableCache ? disableCache : false}
>
{(props: IItemRendererProps) => <FileTreeItem
item={props.item}
itemType={props.itemType}
decorations={decorations.getDecorations(props.item as any)}
onClick={this.handleItemClicked}
onDoubleClick={this.handleItemDoubleClicked}
onContextMenu={this.handleItemCtxMenu}
changeDirectoryCount={this.changeDirectoryCount}
events={this.events}/>}
</FileTree>
)}
</AutoSizer>
</div>
}
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
// 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 }

View File

@ -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)
}
}

View File

@ -0,0 +1,3 @@
export { FileTreeX } from './FileTreeX'
export { TreeModelX } from './TreeModelX'
export { IFileTreeXHandle, IFileTreeXProps, FileTreeXEvent, IFileTreeXTriggerEvents } from './types'

View File

@ -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;
}
}

View File

@ -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)
}
}

View File

@ -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<boolean>
}
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<boolean>
/**
* Amalgam of unix's `mkdir` and `touch` command
*/
create: (path: string, type: FileType) => IFileEntryItem | Promise<IFileEntryItem>
onReady?: (handle: IFileTreeXHandle) => void
onEvent?: (event: IFileTreeXTriggerEvents) => void
onContextMenu?: (ev: React.MouseEvent, item?: FileOrDir) => void
}
export enum FileTreeXEvent {
OnBlur,
onTreeEvents,
}

View File

@ -253,7 +253,7 @@ export class Tree {
}
itemFrom(domElem) {
return this.tree.getItemFromDOM(domElem[0]);
return this.tree.getItemFromDOM(domElem);
}
DOMFrom(item) {

View File

@ -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<IFileEntryItem> => {
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<boolean> => {
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<IFileEntryItem> => {
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<boolean> => {
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<boolean> => {
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<boolean> => {
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(
<BrowserTree model={treeModelX}
onReady={itemHandle} create={create} remove={remove} update={update} />
, 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 <PgMenuDivider key={i}/>;
}
const hasCheck = typeof menuItem.checked == 'boolean';
// Render Browser Tree
await render(
<FileTreeX model={treeModelX}
onReady={itemHandle} create={create} remove={remove} update={update} height={'100%'} disableCache={true} />
, document.getElementById('tree'));
return <PgMenuItem
key={i}
disabled={menuItem.isDisabled}
onClick={()=>{
menuItem.callback();
}}
hasCheck={hasCheck}
checked={menuItem.checked}
>{menuItem.label}</PgMenuItem>;
};
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 (
<Theme>
<FileTreeX
{...props} height={'100%'} disableCache={true} onContextMenu={onContextMenu} />
<PgMenu
anchorPoint={{
x: contextPos?.x,
y: contextPos?.y
}}
open={Boolean(contextPos) && contextMenuItems.length !=0}
onClose={()=>setContextPos(null)}
label="context"
>
{contextMenuItems.length !=0 && contextMenuItems.map((menuItem, i)=>{
const submenus = menuItem.getMenuItems();
if(submenus) {
return <PgSubMenu key={i} label={menuItem.label}>
{submenus.map((submenuItem, si)=>{
return getPgMenuItem(submenuItem, si);
})}
</PgSubMenu>;
}
return getPgMenuItem(menuItem, i);
})}
</PgMenu>
</Theme>
)
}
module.exports = {

View File

@ -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;
}

View File

@ -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';

View File

@ -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)

View File

@ -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()

View File

@ -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 = \

View File

@ -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',
],

View File

@ -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'),

View File

@ -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"