- Move pgadmin4-treeview to pgAdmin main repo.
- Use react based context menu for browser tree. #5615. - Fix feature tests failure.pull/5696/head
parent
64af035ce9
commit
5c34c10d4e
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 }
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export { FileTreeX } from './FileTreeX'
|
||||
export { TreeModelX } from './TreeModelX'
|
||||
export { IFileTreeXHandle, IFileTreeXProps, FileTreeXEvent, IFileTreeXTriggerEvents } from './types'
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -253,7 +253,7 @@ export class Tree {
|
|||
}
|
||||
|
||||
itemFrom(domElem) {
|
||||
return this.tree.getItemFromDOM(domElem[0]);
|
||||
return this.tree.getItemFromDOM(domElem);
|
||||
}
|
||||
|
||||
DOMFrom(item) {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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 = \
|
||||
|
|
|
@ -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',
|
||||
],
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue