diff --git a/packages/ra-tree-core/src/TreeController.ts b/packages/ra-tree-core/src/TreeController.ts index f8cffb3f57c..0ffcdba62c3 100644 --- a/packages/ra-tree-core/src/TreeController.ts +++ b/packages/ra-tree-core/src/TreeController.ts @@ -1,6 +1,9 @@ import { Component, ReactElement, ComponentType } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; +import compose from 'recompose/compose'; +import inflection from 'inflection'; +import { Identifier, withTranslate, Translate } from 'ra-core'; import { getTreeRootNodes } from './selectors'; import { crudGetTreeRootNodes as crudGetTreeRootNodesAction, @@ -8,7 +11,6 @@ import { expandNode as expandNodeAction, toggleNode as toggleNodeAction, } from './actions'; -import { Identifier } from 'ra-core'; export type NodeFunction = (nodeId: Identifier) => void; @@ -41,6 +43,7 @@ interface InjectedProps { hasList: boolean; hasShow: boolean; resource: string; + translate: Translate; } interface StateProps { @@ -106,12 +109,24 @@ export class TreeControllerView extends Component< expandNode, parentSource, resource, - toggleNode, rootNodes, + toggleNode, + translate, ...props } = this.props; + const resourceName = translate(`resources.${resource}.name`, { + smart_count: 2, + _: inflection.humanize(inflection.pluralize(resource)), + }); + const defaultTitle = translate('ra.page.list', { + name: resourceName, + }); + + console.log({ loading: props.loading }); + return children({ + defaultTitle, parentSource, nodes: rootNodes, closeNode: this.handleCloseNode, @@ -129,18 +144,17 @@ const mapStateToProps = (state, { resource }) => ({ version: state.admin.ui.viewVersion, }); -const TreeController = connect< - StateProps, - DispatchProps, - Props & InjectedProps ->( - mapStateToProps, - { - crudGetTreeRootNodes: crudGetTreeRootNodesAction, - closeNode: closeNodeAction, - expandNode: expandNodeAction, - toggleNode: toggleNodeAction, - } +const TreeController = compose( + connect( + mapStateToProps, + { + crudGetTreeRootNodes: crudGetTreeRootNodesAction, + closeNode: closeNodeAction, + expandNode: expandNodeAction, + toggleNode: toggleNodeAction, + } + ), + withTranslate )(TreeControllerView); TreeController.defaultProps = { diff --git a/packages/ra-tree-ui-materialui/src/PlaceHolderTree.tsx b/packages/ra-tree-ui-materialui/src/PlaceHolderTree.tsx deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/ra-tree-ui-materialui/src/TreeList.tsx b/packages/ra-tree-ui-materialui/src/TreeList.tsx index c94127306e0..67de2389ed8 100644 --- a/packages/ra-tree-ui-materialui/src/TreeList.tsx +++ b/packages/ra-tree-ui-materialui/src/TreeList.tsx @@ -1,8 +1,97 @@ -import React from 'react'; +import React, { cloneElement } from 'react'; +import classnames from 'classnames'; +import Card from '@material-ui/core/Card'; +import { createStyles } from '@material-ui/core/styles'; +import { Title } from 'ra-ui-materialui'; + import TreeNodeList from './TreeNodeList'; +import TreeListLoading from './TreeListLoading'; +import TreeListActions from './TreeListActions'; +import TreeListToolbar from './TreeListToolbar'; -const TreeList = ({ children, ...props }) => ( - {children} +const TreeList = ({ + actions = , + aside, + children, + className, + classes, + defaultTitle, + exporter, + filter, + loading, + title, + version, + ...props +}) => ( +
+ + <Card className={classes.card}> + {actions && ( + <TreeListToolbar + {...props} + actions={actions} + exporter={exporter} + permanentFilter={filter} + /> + )} + {loading && props.nodes.length === 0 ? ( + <TreeListLoading /> + ) : ( + <div key={version}> + <TreeNodeList {...props}>{children}</TreeNodeList> + </div> + )} + </Card> + {aside && cloneElement(aside, props)} + </div> ); export default TreeList; + +export const styles = createStyles({ + root: { + display: 'flex', + flex: 1, + }, + card: { + position: 'relative', + flex: '1 1 auto', + }, + actions: { + zIndex: 2, + display: 'flex', + justifyContent: 'flex-end', + flexWrap: 'wrap', + }, + noResults: { padding: 20 }, +}); + +const sanitizeRestProps = ({ + basePath, + children, + classes, + closeNode, + data, + expandNode, + hasCreate, + hasEdit, + hasList, + hasShow, + history, + loading, + locale, + location, + match, + nodes, + options, + parentSource, + permissions, + positionSource, + resource, + toggleNode, + version, + ...rest +}: any) => rest; diff --git a/packages/ra-tree-ui-materialui/src/TreeListActions.tsx b/packages/ra-tree-ui-materialui/src/TreeListActions.tsx new file mode 100644 index 00000000000..6a390f8ec65 --- /dev/null +++ b/packages/ra-tree-ui-materialui/src/TreeListActions.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import onlyUpdateForKeys from 'recompose/onlyUpdateForKeys'; +import { sanitizeListRestProps } from 'ra-core'; +import { CardActions, CreateButton, ExportButton } from 'react-admin'; + +const TreeListActions = ({ + basePath, + className, + closeNode, + currentSort, + expandNode, + exporter, + hasCreate, + hasEdit, + hasList, + hasShow, + parentSource, + permanentFilter, + positionSource, + resource, + toggleNode, + total, + ...rest +}) => ( + <CardActions className={className} {...sanitizeListRestProps(rest)}> + {hasCreate && <CreateButton basePath={basePath} />} + {exporter !== false && ( + <ExportButton + disabled={total === 0} + resource={resource} + sort={currentSort} + filter={permanentFilter} + exporter={exporter} + /> + )} + </CardActions> +); + +TreeListActions.propTypes = { + basePath: PropTypes.string, + className: PropTypes.string, + exporter: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]), + hasCreate: PropTypes.bool, + resource: PropTypes.string, +}; + +export default onlyUpdateForKeys(['resource'])(TreeListActions); diff --git a/packages/ra-tree-ui-materialui/src/TreeListLoading.tsx b/packages/ra-tree-ui-materialui/src/TreeListLoading.tsx new file mode 100644 index 00000000000..a1ab0c0a00f --- /dev/null +++ b/packages/ra-tree-ui-materialui/src/TreeListLoading.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import List from '@material-ui/core/List'; +import { createStyles, withStyles } from '@material-ui/core/styles'; +import TreeNode from './TreeNode'; + +const times = (nbChildren, fn) => + Array.from({ length: nbChildren }, (_, key) => fn(key)); + +const DummyRecord = {}; + +const TreeListLoading = ({ nbFakeLines = 5, ...props }) => ( + <List dense disablePadding {...sanitizeRestProps(props)}> + {times(nbFakeLines, key => ( + // @ts-ignore + <TreeNode key={key} record={DummyRecord} {...props}> + <Placeholder /> + </TreeNode> + ))} + </List> +); + +export default TreeListLoading; + +const sanitizeRestProps = ({ + basePath, + children, + classes, + closeNode, + data, + expandNode, + hasCreate, + hasEdit, + hasList, + hasShow, + history, + loading, + locale, + location, + match, + nodes, + options, + parentSource, + permissions, + positionSource, + resource, + toggleNode, + version, + ...rest +}: any) => rest; + +const RawPlaceholder = ({ classes }) => ( + <div className={classes.root}> </div> +); + +const styles = theme => + createStyles({ + root: { + backgroundColor: theme.palette.grey[300], + width: 256, + }, + }); + +const Placeholder = withStyles(styles)(RawPlaceholder); diff --git a/packages/ra-tree-ui-materialui/src/TreeListToolbar.tsx b/packages/ra-tree-ui-materialui/src/TreeListToolbar.tsx new file mode 100644 index 00000000000..d7777bbf2ae --- /dev/null +++ b/packages/ra-tree-ui-materialui/src/TreeListToolbar.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Toolbar from '@material-ui/core/Toolbar'; +import { withStyles, createStyles } from '@material-ui/core/styles'; + +const styles = createStyles({ + toolbar: { + justifyContent: 'space-between', + }, +}); + +const TreeListToolbar = ({ + classes, + filters, + permanentFilter, // set in the List component by the developer + actions, + exporter, + ...rest +}) => ( + <Toolbar className={classes.toolbar}> + <span /> + {actions && + React.cloneElement(actions, { + ...rest, + className: classes.actions, + exporter, + permanentFilter, + ...actions.props, + })} + </Toolbar> +); + +TreeListToolbar.propTypes = { + classes: PropTypes.object, + permanentFilter: PropTypes.object, + actions: PropTypes.element, + exporter: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]), +}; + +TreeListToolbar.defaultProps = { + classes: {}, +}; + +export default withStyles(styles)(TreeListToolbar); diff --git a/packages/ra-tree-ui-materialui/src/TreeNode.tsx b/packages/ra-tree-ui-materialui/src/TreeNode.tsx index 5fc4a2454f6..7b6b91f6445 100644 --- a/packages/ra-tree-ui-materialui/src/TreeNode.tsx +++ b/packages/ra-tree-ui-materialui/src/TreeNode.tsx @@ -116,7 +116,7 @@ class TreeNode extends Component<Props & WithStyles<typeof styles>> { <div className={classes.icon}> <CircularProgress size="1em" /> </div> - ) : nodes.length === 0 ? ( + ) : !nodes || nodes.length === 0 ? ( <div className={classes.icon}> <KeyboardArrowRight /> </div> @@ -183,7 +183,7 @@ const styles = (theme: Theme): StyleRules => ({ container: { alignItems: 'center', display: 'inline-flex', - justifyContent: 'baseline', + justifyContent: 'center', verticalAlign: 'middle', }, content: { diff --git a/packages/ra-tree-ui-materialui/src/index.ts b/packages/ra-tree-ui-materialui/src/index.ts index cc2603396b6..4e9ecf37cbf 100644 --- a/packages/ra-tree-ui-materialui/src/index.ts +++ b/packages/ra-tree-ui-materialui/src/index.ts @@ -1,5 +1,6 @@ import Tree from './Tree'; import TreeNode from './TreeNode'; import TreeList from './TreeList'; -export { TreeNode, Tree, TreeList }; +import TreeListLoading from './TreeListLoading'; +export { TreeNode, Tree, TreeList, TreeListLoading }; export * from 'ra-tree-core';