var classNames = require('classnames'); var AppActions = require('../actions/AppActions'); var Files = require('../stores/Files'); var React = require('react'); var ReactBootstrap = require('react-bootstrap'); var Glyphicon = ReactBootstrap.Glyphicon; // The DepotTree is a React tree control that expects to interact with a // server API that doesn't like to provide tree data, just lists of one // level at a time. // // This control creates it's own cache of the available depot, since our 'store' // doesn't really know much about the existing server. What this means, is that // all directories are generally expandable, even if they don't have children, // and we won't know if the dir contains anything until they are expanded. // // Properties: // - dirsOnly: when true, will not display files // var DepotTree = React.createClass({ getInitialState: function() { return { depots: [], selected: null }; }, componentDidMount: function() { Files.addDepotsListedListener(this.handleDepotsListed); Files.addDirListedListener(this.handleDirListed); AppActions.listDepots(); }, componentWillUnmount: function() { Files.removeDepotsListedListener(this.handleDepotsListed); Files.removeDirListedListener(this.handleDirListed); }, render: function() { var self = this; return ( <div className='depot-tree'> {this.state.depots.map(function(x) { return renderItem(self, x); })} </div> ); }, handleDepotsListed: function(depots) { depots.sort(function(x, y) { return x.name.localeCompare(y.name); }); this.setState({ depots: depots }); }, handleDirListed: function(dirPath, items) { var dir = findDir(this.state.depots, dirPath); items.forEach(function(i) { dir.insert(i) }); dir.sortChildren(); dir.loaded = true; this.setState({ depots: this.state.depots }); }, openFolder: function(dir, evt) { dir.expanded = true; if (!dir.loaded) { AppActions.listDir(dir.pathId.join('/')); } this.setState({ depots: this.state.depots }); evt.stopPropagation(); }, closeFolder: function(dir, evt) { dir.expanded = false; this.setState({ depots: this.state.depots }); evt.stopPropagation(); }, clickItem: function(dir, evt) { var cur = this.state.selected; if (cur && cur.pathId.join('/') == dir.pathId.join('/')) { // Same node dir.selected = false; this.state.selected = null; } else { if (cur) { cur.selected = false; } dir.selected = true; this.state.selected = dir; } if (this.state.selected && typeof this.props.onSelect == "function") { this.props.onSelect(this.state.selected); } this.setState({ depots: this.state.depots }); evt.stopPropagation(); } }); //---------------------------------------------------------------------------- // Local (private) helper methods //---------------------------------------------------------------------------- // Render a row of our depot tree, and all children function renderItem(view, item) { if (view.props.dirsOnly && !item.children) { return null; } var children = null; if (item.children && item.expanded) { children = item.children.map(function(child) { return renderItem(view, child); }); } var control = renderControl(view, item); var icon = renderIcon(item); var text = renderText(item); var classes = classNames({ 'depot-tree-item': true, 'selected': item.selected }); return ( <div key={item.pathId.join('-')} className={classes} onClick={view.clickItem.bind(view, item)}> <div className='depot-tree-item-value'> {control} {icon} {text} </div> <div className='depot-tree-children'> {children} </div> </div> ) } // The control depends on the 'expanded' attribute being set to 'true'. function renderControl(view, item) { if (item.children) { if (item.expanded) { return <Glyphicon glyph='triangle-bottom' onClick={view.closeFolder.bind(view, item)} />; } else { return <Glyphicon glyph='triangle-right' onClick={view.openFolder.bind(view, item)} />; } } } function renderIcon(item) { if (item.children) { if (item.expanded) { return <Glyphicon glyph='folder-open' /> } else { return <Glyphicon glyph='folder-close' /> } } else { return <Glyphicon glyph='file' /> } } // We *might* want to render folders in a different font than files. We'll see. function renderText(item) { return <span>{ item.name }</span>; } function findDir(depots, dirPath) { if (dirPath.startsWith('//')) { dirPath = dirPath.substring(2); } var paths = dirPath.split('/'); return findDirRecurse(depots, paths, 0); } function findDirRecurse(items, paths, level) { var curName = paths[level]; var item = items.find(function(x) { return x.name == curName; }); // Halting condition. Item can be null, in which case we've not found it. if (level == (paths.length - 1)) { return item; } if (item && item.children) { return findDirRecurse(item.children, paths, level + 1); } return null; } module.exports = DepotTree;
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#1 | 15688 | Doug Scheirer |
Populate -o //guest/perforce_software/helix-web-services/... //guest/doug_scheirer/helix-web-services/.... |
||
//guest/perforce_software/helix-web-services/main/source/helix_web_components/project_management/components/DepotTree.jsx | |||||
#1 | 15622 | tjuricek |
Move source code to 'source/' subdirectory of branch. build/ will remain where it is. |
||
//guest/perforce_software/helix-web-services/main/helix_web_components/project_management/components/DepotTree.jsx | |||||
#2 | 14151 | tjuricek |
Add depot tree control and selection to the create projects page. Styling and error checking is kept to a minimum for the time being. Our goal is just internal workflow and feedback. |
||
#1 | 14108 | tjuricek |
Added models for handling Perforce server depot listing and traversal. This is not complete, however, the models are a start to making it easy to generate a tree control. (Most tree controls in the wild assume you know the tree structure from the start, which is not true in our case.) The tricky bit is making it easy to build the tree out given that you're visiting only one directory at a time. |