Skip to content

Commit b97fc46

Browse files
Ensure double click event is not ignored in the browser tree.
1 parent cae212e commit b97fc46

File tree

3 files changed

+129
-68
lines changed

3 files changed

+129
-68
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import cn from 'classnames';
2+
import { useSingleAndDoubleClick } from '../../../custom_hooks';
3+
import { FileEntry, ItemType, FileType} from 'react-aspen';
4+
import * as React from 'react';
5+
import PropTypes from 'prop-types';
6+
7+
export default function FileTreeItemComponent({item, itemType, decorations, handleContextMenu, handleDragStartItem, handleMouseEnter, handleMouseLeave, handleItemClicked, handleItemDoubleClicked, handleDivRef}){
8+
const onClick = useSingleAndDoubleClick(handleItemClicked, handleItemDoubleClicked) ;
9+
const isRenamePrompt = itemType === ItemType.RenamePrompt;
10+
const isNewPrompt = itemType === ItemType.NewDirectoryPrompt || itemType === ItemType.NewFilePrompt;
11+
const isDirExpanded = itemType === ItemType.Directory
12+
? item.expanded
13+
: itemType === ItemType.RenamePrompt && item.target.type === FileType.Directory
14+
? item.target.expanded
15+
: false;
16+
const fileOrDir =
17+
(itemType === ItemType.File ||
18+
itemType === ItemType.NewFilePrompt ||
19+
(itemType === ItemType.RenamePrompt && (item).target.constructor === FileEntry))
20+
? 'file'
21+
: 'directory';
22+
23+
if (item.parent?.parent && item.parent?.path) {
24+
item.resolvedPathCache = item.parent.path + '/' + item._metadata.data.id;
25+
}
26+
27+
const itemChildren = item.children && item.children.length > 0 && item._metadata.data._type.indexOf('coll-') !== -1 ? '(' + item.children.length + ')' : '';
28+
const extraClasses = item._metadata.data.extraClasses ? item._metadata.data.extraClasses.join(' ') : '';
29+
const tags = item._metadata.data?.tags ?? [];
30+
31+
return(
32+
<div
33+
className={cn('file-entry', {
34+
renaming: isRenamePrompt,
35+
prompt: isRenamePrompt || isNewPrompt,
36+
new: isNewPrompt,
37+
}, fileOrDir, decorations ? decorations.classlist : null, `depth-${item.depth}`, extraClasses)}
38+
data-depth={item.depth}
39+
onContextMenu={handleContextMenu}
40+
onClick={onClick}
41+
onDragStart={handleDragStartItem}
42+
onMouseEnter={handleMouseEnter}
43+
onMouseLeave={handleMouseLeave}
44+
onKeyDown={()=>{/* taken care by parent */}}
45+
// required for rendering context menus when opened through context menu button on keyboard
46+
ref={handleDivRef}
47+
draggable={true}>
48+
49+
{!isNewPrompt && fileOrDir === 'directory' ?
50+
<i className={cn('directory-toggle', isDirExpanded ? 'open' : '')} />
51+
: null
52+
}
53+
54+
<span className='file-label'>
55+
{
56+
item._metadata?.data?.icon ?
57+
<i className={cn('file-icon', item._metadata?.data?.icon ? item._metadata.data.icon : fileOrDir)} /> : null
58+
}
59+
<span className='file-name'>
60+
{ _.unescape(item._metadata?.data._label)}
61+
</span>
62+
<span className='children-count'>{itemChildren}</span>
63+
{tags.map((tag)=>(
64+
<div key={tag.text} className='file-tag' style={{'--tag-color': tag.color}}>
65+
{tag.text}
66+
</div>
67+
))}
68+
</span>
69+
</div>);
70+
71+
}
72+
73+
FileTreeItemComponent.propTypes = {
74+
item: PropTypes.object,
75+
itemType: PropTypes.number,
76+
decorations: PropTypes.object,
77+
handleContextMenu: PropTypes.func,
78+
handleDragStartItem:PropTypes.func,
79+
handleMouseEnter:PropTypes.func,
80+
handleMouseLeave:PropTypes.func,
81+
handleItemClicked:PropTypes.func,
82+
handleItemDoubleClicked:PropTypes.func,
83+
handleDivRef:PropTypes.func
84+
85+
};

web/pgadmin/static/js/components/PgTree/FileTreeItem/index.tsx

Lines changed: 17 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,12 @@
77
//
88
//////////////////////////////////////////////////////////////
99

10-
import cn from 'classnames';
1110
import * as React from 'react';
1211
import { ClasslistComposite } from 'aspen-decorations';
13-
import { Directory, FileEntry, IItemRendererProps, ItemType, RenamePromptHandle, FileType, FileOrDir} from 'react-aspen';
12+
import { Directory, FileEntry, IItemRendererProps, ItemType, FileOrDir} from 'react-aspen';
1413
import {IFileTreeXTriggerEvents, FileTreeXEvent } from '../types';
15-
import _ from 'lodash';
1614
import { Notificar } from 'notificar';
17-
15+
import FileTreeItemComponent from './FileTreeItemComponent';
1816
interface IItemRendererXProps {
1917
/**
2018
* In this implementation, decoration are null when item is `PromptHandle`
@@ -58,70 +56,21 @@ export class FileTreeItem extends React.Component<IItemRendererXProps & IItemRen
5856

5957
public render() {
6058
const { item, itemType, decorations } = this.props;
61-
62-
const isRenamePrompt = itemType === ItemType.RenamePrompt;
63-
const isNewPrompt = itemType === ItemType.NewDirectoryPrompt || itemType === ItemType.NewFilePrompt;
64-
const isDirExpanded = itemType === ItemType.Directory
65-
? (item as Directory).expanded
66-
: itemType === ItemType.RenamePrompt && (item as RenamePromptHandle).target.type === FileType.Directory
67-
? ((item as RenamePromptHandle).target as Directory).expanded
68-
: false;
69-
70-
const fileOrDir =
71-
(itemType === ItemType.File ||
72-
itemType === ItemType.NewFilePrompt ||
73-
(itemType === ItemType.RenamePrompt && (item as RenamePromptHandle).target.constructor === FileEntry))
74-
? 'file'
75-
: 'directory';
76-
77-
if (this.props.item.parent?.parent && this.props.item.parent?.path) {
78-
this.props.item.resolvedPathCache = this.props.item.parent.path + '/' + this.props.item._metadata.data.id;
79-
}
80-
81-
const itemChildren = item.children && item.children.length > 0 && item._metadata.data._type.indexOf('coll-') !== -1 ? '(' + item.children.length + ')' : '';
82-
const extraClasses = item._metadata.data.extraClasses ? item._metadata.data.extraClasses.join(' ') : '';
83-
84-
const tags = item._metadata.data?.tags ?? [];
85-
86-
return (
87-
<div
88-
className={cn('file-entry', {
89-
renaming: isRenamePrompt,
90-
prompt: isRenamePrompt || isNewPrompt,
91-
new: isNewPrompt,
92-
}, fileOrDir, decorations ? decorations.classlist : null, `depth-${item.depth}`, extraClasses)}
93-
data-depth={item.depth}
94-
onContextMenu={this.handleContextMenu}
95-
onClick={this.handleClick}
96-
onDragStart={this.handleDragStartItem}
97-
onMouseEnter={this.handleMouseEnter}
98-
onMouseLeave={this.handleMouseLeave}
99-
onKeyDown={()=>{/* taken care by parent */}}
100-
// required for rendering context menus when opened through context menu button on keyboard
101-
ref={this.handleDivRef}
102-
draggable={true}>
103-
104-
{!isNewPrompt && fileOrDir === 'directory' ?
105-
<i className={cn('directory-toggle', isDirExpanded ? 'open' : '')} />
106-
: null
107-
}
108-
109-
<span className='file-label' onDoubleClick={this.handleDoubleClick}>
110-
{
111-
item._metadata?.data?.icon ?
112-
<i className={cn('file-icon', item._metadata?.data?.icon ? item._metadata.data.icon : fileOrDir)} /> : null
113-
}
114-
<span className='file-name'>
115-
{ _.unescape(this.props.item.getMetadata('data')._label)}
116-
</span>
117-
<span className='children-count'>{itemChildren}</span>
118-
{tags.map((tag)=>(
119-
<div key={tag.text} className='file-tag' style={{'--tag-color': tag.color} as React.CSSProperties}>
120-
{tag.text}
121-
</div>
122-
))}
123-
</span>
124-
</div>);
59+
return(
60+
<div>
61+
<FileTreeItemComponent
62+
item={item}
63+
itemType={itemType}
64+
decorations={decorations}
65+
handleContextMenu={this.handleContextMenu}
66+
handleDragStartItem={this.handleDragStartItem}
67+
handleMouseEnter={this.handleMouseEnter}
68+
handleMouseLeave={this.handleMouseLeave}
69+
handleItemClicked={this.handleClick}
70+
handleItemDoubleClicked={this.handleDoubleClick}
71+
handleDivRef={this.handleDivRef}/>
72+
</div>
73+
);
12574
}
12675

12776
public componentDidMount() {

web/pgadmin/static/js/custom_hooks.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,33 @@ export function useInterval(callback, delay) {
2929
}, [delay]);
3030
}
3131

32+
/* React hook for handling double and single click events */
33+
export function useSingleAndDoubleClick(handleSingleClick, handleDoubleClick, delay = 250) {
34+
const [state, setState] = useState({ click: 0, props: undefined });
35+
36+
useEffect(() => {
37+
const timer = setTimeout(() => {
38+
// simple click
39+
if (state.click === 1){
40+
handleSingleClick(state.props);
41+
setState({ click: 0, props: state.props });
42+
}
43+
}, delay);
44+
45+
if (state.click === 2) {
46+
handleDoubleClick(state.props);
47+
setState({ click: 0, props: state.props });
48+
}
49+
50+
return () => clearTimeout(timer);
51+
}, [state, handleSingleClick, handleDoubleClick, delay ]);
52+
53+
return (props) => {
54+
setState((prevState) => ({ click: prevState.click + 1, props }));
55+
};
56+
}
57+
58+
3259
export function useDelayedCaller(callback) {
3360
let timer;
3461
useEffect(() => {

0 commit comments

Comments
 (0)