import * as React from 'react' import { FileTree, Directory, FileEntry, ItemType, IFileTreeHandle, WatchEvent, FileType, IItemRendererProps, FileOrDir } from 'react-aspen' import { Decoration, TargetMatchMode } from 'aspen-decorations' import { FileTreeItem } from '../FileTreeItem' import { Notificar, DisposablesComposite } from 'notificar' import { IFileTreeXHandle, IFileTreeXProps, FileTreeXEvent, IFileTreeXTriggerEvents } from '../types' import { KeyboardHotkeys } from '../services/keyboardHotkeys' import { TreeModelX } from '../TreeModelX' import AutoSizer from "react-virtualized-auto-sizer"; export class FileTreeX extends React.Component { private fileTreeHandle: IFileTreeXHandle private activeFileDec: Decoration private pseudoActiveFileDec: Decoration private activeFile: FileOrDir private pseudoActiveFile: FileOrDir private wrapperRef: React.RefObject = React.createRef() private events: Notificar private disposables: DisposablesComposite private keyboardHotkeys: KeyboardHotkeys private fileTreeEvent: IFileTreeXTriggerEvents constructor(props: IFileTreeXProps) { super(props) this.events = new Notificar() this.disposables = new DisposablesComposite() this.activeFileDec = new Decoration('active') this.pseudoActiveFileDec = new Decoration('pseudo-active') } render() { const { height, width, model, disableCache } = this.props const { decorations } = model return
{({ width, height }) => ( {(props: IItemRendererProps) => } )}
} public componentDidMount() { for(let child of this.props.model.root.children) { this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'loaded', child) } } componentWillUnmount() { const { model } = this.props model.decorations.removeDecoration(this.activeFileDec) model.decorations.removeDecoration(this.pseudoActiveFileDec) this.disposables.dispose() } private handleTreeEvent = (event: IFileTreeXTriggerEvents) => { this.fileTreeEvent = this.props.onEvent } private handleTreeReady = (handle: IFileTreeHandle) => { const { onReady, model } = this.props const scrollDiv = this.wrapperRef.current?.querySelector('div')?.querySelector('div') if(this.props.onScroll) { scrollDiv?.addEventListener('scroll', (ev: any)=>this.props.onScroll?.(ev)); } this.fileTreeHandle = { ...handle, getModel: () => this.props.model, getActiveFile: () => this.activeFile, setActiveFile: this.setActiveFile, getPseudoActiveFile: () => this.pseudoActiveFile, setPseudoActiveFile: this.setPseudoActiveFile, toggleDirectory: this.toggleDirectory, closeDir: this.closeDir, remove: this.removeDir, newFile: async (dirOrPath: Directory | string) => this.supervisePrompt(await handle.promptNewFile(dirOrPath as any)), newFolder: async (dirOrPath: Directory | string) => this.supervisePrompt(await handle.promptNewDirectory(dirOrPath as any)), onBlur: (callback) => this.events.add(FileTreeXEvent.OnBlur, callback), hasDirectFocus: () => this.wrapperRef.current === document.activeElement, first: this.first, parent: this.parent, hasParent: this.hasParent, isOpen: this.isOpen, isClosed: this.isClosed, itemData: this.itemData, children: this.children, getItemFromDOM: this.getItemFromDOM, getDOMFromItem: this.getDOMFromItem, onTreeEvents: (callback) => this.events.add(FileTreeXEvent.onTreeEvents, callback), addIcon: this.addIcon, addCssClass: this.addCssClass, create: this.create, remove: this.remove, update: this.update, refresh: this.refresh, setLabel: this.setLabel, unload: this.unload, deSelectActiveFile: this.deSelectActiveFile, resize: this.resize, showLoader: this.showLoader, hideLoader: this.hideLoader, } model.decorations.addDecoration(this.activeFileDec) model.decorations.addDecoration(this.pseudoActiveFileDec) this.disposables.add(this.fileTreeHandle.onDidChangeModel((prevModel: TreeModelX, newModel: TreeModelX) => { this.setActiveFile(null) this.setPseudoActiveFile(null) prevModel.decorations.removeDecoration(this.activeFileDec) prevModel.decorations.removeDecoration(this.pseudoActiveFileDec) newModel.decorations.addDecoration(this.activeFileDec) newModel.decorations.addDecoration(this.pseudoActiveFileDec) })) this.disposables.add(this.fileTreeHandle.onBlur(() => { this.setPseudoActiveFile(null) })) this.keyboardHotkeys = new KeyboardHotkeys(this.fileTreeHandle) if (typeof onReady === 'function') { onReady(this.fileTreeHandle) } } private setActiveFile = async (fileOrDirOrPath: FileOrDir | string, ensureVisible, align): Promise => { const fileH = typeof fileOrDirOrPath === 'string' ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath) : fileOrDirOrPath if (fileH === this.props.model.root) { return } if (this.activeFile !== fileH) { if (this.activeFile) { this.activeFileDec.removeTarget(this.activeFile) } if (fileH) { this.activeFileDec.addTarget(fileH as any, TargetMatchMode.Self) } this.activeFile = fileH this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'selected', fileH) if (fileH && ensureVisible === true) { const alignTree = align !== undefined && align !== null ? align : 'auto' await this.fileTreeHandle.ensureVisible(fileH, alignTree) } } } private ensureVisible = async (fileOrDirOrPath: FileOrDir | string): Promise => { const fileH = typeof fileOrDirOrPath === 'string' ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath) : fileOrDirOrPath if (fileH) { await this.fileTreeHandle.ensureVisible(fileH) } } private deSelectActiveFile = async (fileOrDirOrPath: FileOrDir | string): Promise => { const fileH = typeof fileOrDirOrPath === 'string' ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath) : fileOrDirOrPath if (fileH === this.props.model.root) { return } if (this.activeFile === fileH) { this.activeFileDec.removeTarget(this.activeFile) this.activeFile = null } } private setPseudoActiveFile = async (fileOrDirOrPath: FileOrDir | string): Promise => { const fileH = typeof fileOrDirOrPath === 'string' ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath) : fileOrDirOrPath if (fileH === this.props.model.root) { return } if (this.pseudoActiveFile !== fileH) { if (this.pseudoActiveFile) { this.pseudoActiveFileDec.removeTarget(this.pseudoActiveFile) } if (fileH) { this.pseudoActiveFileDec.addTarget(fileH as any, TargetMatchMode.Self) } this.pseudoActiveFile = fileH } if (fileH) { await this.fileTreeHandle.ensureVisible(fileH) } this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'selected', fileH) } private create = async (parentDir, itemData): Promise => { if (parentDir == undefined || parentDir == null) { parentDir = this.props.model.root } const {create, model } = this.props const isOpen = parentDir.isExpanded let maybeFile = undefined if (isOpen && (parentDir._children == null || parentDir._children.length == 0)) { await this.fileTreeHandle.closeDirectory(parentDir as Directory) } if (!parentDir.isExpanded && (parentDir._children == null || parentDir._children.length == 0)) { await this.fileTreeHandle.openDirectory(parentDir as Directory) } else { await this.fileTreeHandle.openDirectory(parentDir as Directory) maybeFile = await create(parentDir.path, itemData) if (maybeFile && maybeFile.type && maybeFile.name) { model.root.inotify({ type: WatchEvent.Added, directory: parentDir.path, file: maybeFile, }) } } this.changeDirectoryCount(parentDir) let newItem = parentDir._children.find((c) => c._metadata.data.id === itemData.id) newItem.resolvedPathCache = newItem.parent.path + "/" + newItem._metadata.data.id return newItem } private update = async (item, itemData): Promise => { item._metadata.data = itemData await this.props.update(item.path, itemData) this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'updated', item) } private refresh = async (item): Promise => { const {remove, model } = this.props const isOpen = item.isExpanded if (item.children && item.children.length > 0) { for(let entry of item.children) { await this.remove(entry).then(val => {}, error => {console.warn("Error removing item")}) } } if (isOpen) { const ref = FileTreeItem.itemIdToRefMap.get(item.id); if (ref) { this.showLoader(ref) } await this.fileTreeHandle.closeDirectory(item as Directory) await this.fileTreeHandle.openDirectory(item as Directory) await this.changeResolvePath(item as Directory) this.changeDirectoryCount(item) if (ref) { this.hideLoader(ref) } } } private unload = async (item): Promise => { const {remove, model } = this.props const isOpen = item.isExpanded if (item.children && item.children.length > 0) { for(let entry of item.children) { await this.remove(entry).then(val => {}, error => {console.warn(error)}) } } if (isOpen) { await this.fileTreeHandle.closeDirectory(item as Directory) this.changeDirectoryCount(item) } } private remove = async (item): Promise => { const {remove, model } = this.props const path = item.path await remove(path, false) const dirName = model.root.pathfx.dirname(path); const fileName = model.root.pathfx.basename(path); const parent = item.parent if (dirName === parent.path) { const item_1 = parent._children.find((c) => c._metadata && c._metadata.data.id === fileName); if (item_1) { parent.unlinkItem(item_1); if (parent._children.length == 0) { parent._children = null } this.changeDirectoryCount(parent) this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'removed', item) } else { console.warn("Item not found") } } } private first = async (fileOrDirOrPath: FileOrDir | string) => { const fileH = typeof fileOrDirOrPath === 'string' ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath) : fileOrDirOrPath if (fileH === undefined || fileH === null) { return this.props.model.root.children[0] } if (fileH.branchSize > 0) { return fileH.children[0] } return null } private parent = async (fileOrDirOrPath: FileOrDir | string) => { const fileH = typeof fileOrDirOrPath === 'string' ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath) : fileOrDirOrPath if (fileH === FileType.Directory || fileH === FileType.File) { return fileH.parent } return null } private hasParent = async (fileOrDirOrPath: FileOrDir | string) => { const fileH = typeof fileOrDirOrPath === 'string' ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath) : fileOrDirOrPath if (fileH === FileType.Directory || fileH === FileType.File) { return fileH.parent ? true : false } return false } private children = async (fileOrDirOrPath: FileOrDir | string) => { const fileH = typeof fileOrDirOrPath === 'string' ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath) : fileOrDirOrPath if (fileH === FileType.Directory) { return fileH.children } return null } private isOpen = async (fileOrDirOrPath: FileOrDir | string) => { const fileH = typeof fileOrDirOrPath === 'string' ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath) : fileOrDirOrPath if (fileH === FileType.Directory) { return fileH.isExpanded } return false } private isClosed = async (fileOrDirOrPath: FileOrDir | string) => { const fileH = typeof fileOrDirOrPath === 'string' ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath) : fileOrDirOrPath if (fileH === FileType.Directory || fileH === FileType.File) { return !fileH.isExpanded } return false } private itemData = async (fileOrDirOrPath: FileOrDir | string) => { const fileH = typeof fileOrDirOrPath === 'string' ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath) : fileOrDirOrPath if (fileH === FileType.Directory || fileH === FileType.File) { return fileH._metadata.data } return null } private setLabel = async(pathOrDir: string | Directory, label: string): Promise => { const dir = typeof pathOrDir === 'string' ? await this.fileTreeHandle.getFileHandle(pathOrDir) : pathOrDir const ref = FileTreeItem.itemIdToRefMap.get(dir.id); if (ref) { ref.style.background = 'none' const label$ = ref.querySelector('span.file-name') as HTMLDivElement if (label$) { if (typeof(label) == "object" && label.label) { label = label.label } label$.innerHTML = label; } } } private changeDirectoryCount = async(pathOrDir: string | Directory): Promise => { const dir = typeof pathOrDir === 'string' ? await this.fileTreeHandle.getFileHandle(pathOrDir) : pathOrDir if (dir.type === FileType.Directory && dir._metadata.data && dir._metadata.data.is_collection === true) { const ref = FileTreeItem.itemIdToRefMap.get(dir.id); if (ref) { ref.style.background = 'none' const label$ = ref.querySelector('span.children-count') as HTMLDivElement if(dir.children && dir.children.length > 0) { label$.innerHTML = "(" + dir.children.length + ")"; } else { label$.innerHTML = ""; } } } } private closeDir = async (pathOrDir: string | Directory) => { const dir = typeof pathOrDir === 'string' ? await this.fileTreeHandle.getFileHandle(pathOrDir) : pathOrDir if (dir.type === FileType.Directory) { if ((dir as Directory).expanded) { this.fileTreeHandle.closeDirectory(dir as Directory) this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'closed', dir) } } } private toggleDirectory = async (pathOrDir: string | Directory) => { const dir = typeof pathOrDir === 'string' ? await this.fileTreeHandle.getFileHandle(pathOrDir) : pathOrDir if (dir.type === FileType.Directory) { if ((dir as Directory).expanded) { this.fileTreeHandle.closeDirectory(dir as Directory) this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'closed', dir) } else { const ref = FileTreeItem.itemIdToRefMap.get(dir.id); if (ref) { this.showLoader(ref) } await this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'beforeopen', dir) await this.fileTreeHandle.openDirectory(dir as Directory) await this.changeResolvePath(dir as Directory) if (ref) { this.hideLoader(ref) } this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'opened', dir) } } } private addIcon = async (pathOrDir: string | Directory, icon) => { const dir = typeof pathOrDir === 'string' ? await this.fileTreeHandle.getFileHandle(pathOrDir) : pathOrDir const ref = FileTreeItem.itemIdToRefMap.get(dir.id); if (ref) { const label$ = ref.querySelector('.file-label i') as HTMLDivElement label$.className = icon.icon; } } private addCssClass = async (pathOrDir: string | Directory, cssClass) => { const dir = typeof pathOrDir === 'string' ? await this.fileTreeHandle.getFileHandle(pathOrDir) : pathOrDir const ref = FileTreeItem.itemIdToRefMap.get(dir.id); if (ref) { ref.classList.add(cssClass) if (!dir._metadata.data.extraClasses) dir._metadata.data.extraClasses = [] dir._metadata.data.extraClasses.push(cssClass) } } private showLoader = (ref: HTMLDivElement) => { // get label ref and add loading class ref.style.background = 'none' const label$ = ref.querySelector('i.directory-toggle') as HTMLDivElement if (label$) label$.classList.add("loading") } private hideLoader = (ref: HTMLDivElement) => { // remove loading class. ref.style.background = 'none' const label$ = ref.querySelector('i.directory-toggle') as HTMLDivElement if (label$) label$.classList.remove("loading") } private handleBlur = () => { this.events.dispatch(FileTreeXEvent.OnBlur) } private handleItemClicked = async (ev: React.MouseEvent, item: FileOrDir, type: ItemType) => { if (type === ItemType.Directory && ev.target.className.includes("directory-toggle")) { await this.toggleDirectory(item as Directory) } await this.setActiveFile(item as FileEntry) } private handleItemDoubleClicked = async (ev: React.MouseEvent, item: FileOrDir, type: ItemType) => { await this.toggleDirectory(item as Directory) await this.setActiveFile(item as FileEntry) } private getItemFromDOM = (clientReact) => { return FileTreeItem.refToItemIdMap.get(clientReact); } private getDOMFromItem = (item: FileOrDir) => { return FileTreeItem.itemIdToRefMap.get(item.id); } private handleClick = (ev: React.MouseEvent) => { // clicked in "blank space" if (ev.currentTarget === ev.target) { this.setPseudoActiveFile(null) } } private handleItemCtxMenu = (ev: React.MouseEvent, item: FileOrDir) => { return this.props.onContextMenu?.(ev, item); } private handleKeyDown = (ev: React.KeyboardEvent) => { return this.keyboardHotkeys.handleKeyDown(ev) } private onResize = (...args) => { if (this.wrapperRef.current != null) { this.resize() } } private resize = (scrollX, scrollY) => { const scrollXPos = scrollX ? scrollX : 0 const scrollYPos = scrollY ? scrollY : this.props.model.state.scrollOffset const div = this.wrapperRef.current.querySelector('div').querySelector('div') as HTMLDivElement div.scroll(scrollXPos, scrollYPos) } private changeResolvePath = async (item: FileOrDir): Promise => { // Change the path as per pgAdmin requirement: Item Id wise if (item.type === FileType.File) { item.resolvedPathCache = item.parent.path + "/" + item._metadata.data.id } if (item.type === FileType.Directory && item.children && item.children.length > 0) { for(let entry of item.children) { entry.resolvedPathCache = entry.parent.path + "/" + entry._metadata.data.id } } } } export { IFileTreeXHandle, IFileTreeXProps }