mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
- Move pgadmin4-treeview to pgAdmin main repo.
- Use react based context menu for browser tree. #5615. - Fix feature tests failure.
This commit is contained in:
@@ -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}
|
||||
|
||||
167
web/pgadmin/static/js/components/PgTree/FileTreeItem/index.tsx
Normal file
167
web/pgadmin/static/js/components/PgTree/FileTreeItem/index.tsx
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
609
web/pgadmin/static/js/components/PgTree/FileTreeX/index.tsx
Normal file
609
web/pgadmin/static/js/components/PgTree/FileTreeX/index.tsx
Normal 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 }
|
||||
10
web/pgadmin/static/js/components/PgTree/TreeModelX/index.ts
Normal file
10
web/pgadmin/static/js/components/PgTree/TreeModelX/index.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
3
web/pgadmin/static/js/components/PgTree/index.ts
Normal file
3
web/pgadmin/static/js/components/PgTree/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { FileTreeX } from './FileTreeX'
|
||||
export { TreeModelX } from './TreeModelX'
|
||||
export { IFileTreeXHandle, IFileTreeXProps, FileTreeXEvent, IFileTreeXTriggerEvents } from './types'
|
||||
400
web/pgadmin/static/js/components/PgTree/scss/styles.scss
Normal file
400
web/pgadmin/static/js/components/PgTree/scss/styles.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
93
web/pgadmin/static/js/components/PgTree/types.ts
Normal file
93
web/pgadmin/static/js/components/PgTree/types.ts
Normal 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,
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user