Skip to content

feat(listView): add ListView components #50

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 18, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6,019 changes: 5,195 additions & 824 deletions package-lock.json

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
{
"name": "patternfly-react",
"version": "0.0.0-semantically-released",
"description": "This library provides a set of common React components for use with the PatternFly reference implementation.",
"description":
"This library provides a set of common React components for use with the PatternFly reference implementation.",
"main": "lib/index.js",
"repository": {
"type": "git",
"url": "https://github.com/patternfly/patternfly-react.git"
},
"keywords": [
"react",
"patternfly"
],
"keywords": ["react", "patternfly"],
"author": "Red Hat",
"license": "Apache-2.0",
"bugs": {
Expand All @@ -24,9 +22,9 @@
"devDependencies": {
"@storybook/addon-actions": "^3.1.2",
"@storybook/addon-info": "^3.1.4",
"@storybook/addon-knobs": "3.1.2",
"@storybook/addon-knobs": "3.1.5",
"@storybook/addon-links": "^3.1.2",
"@storybook/react": "3.1.3",
"@storybook/react": "3.1.5",
"@storybook/storybook-deployer": "^2.0.0",
"babel-cli": "^6.24.0",
"babel-core": "^6.24.0",
Expand Down Expand Up @@ -62,16 +60,18 @@
},
"scripts": {
"commit": "git-cz",
"build": "babel src --out-dir lib --ignore test.js",
"build": "babel src --out-dir lib --ignore test.js,__mocks__ ",
"lint": "eslint --max-warnings 0 src storybook",
"prettier": "prettier --write --single-quote --no-semi '{src,storybook}/**/*.js'",
"prettier":
"prettier --write --single-quote --no-semi '{src,storybook}/**/*.js'",
"prepublish": "npm run build",
"test": "npm run lint && jest",
"test:watch": "jest --watchAll",
"storybook": "start-storybook -c storybook -p 6006",
"build-storybook": "build-storybook -c storybook -o .out",
"storybook:deploy": "storybook-to-ghpages",
"semantic-release": "semantic-release pre && npm publish && semantic-release post"
"semantic-release":
"semantic-release pre && npm publish && semantic-release post"
},
"czConfig": {
"path": "node_modules/cz-conventional-changelog"
Expand Down
193 changes: 193 additions & 0 deletions src/ListView/ListView.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import cx from 'classnames'
import React from 'react'
import { storiesOf } from '@storybook/react'
import { Row, Col } from 'react-bootstrap'
import { withKnobs, boolean } from '@storybook/addon-knobs'
import { defaultTemplate } from '../../storybook/decorators/storyTemplates'

import {
Button,
DropdownKebab,
ListView,
ListViewInfoItem,
ListViewItem,
ListViewIcon,
MenuItem
} from '../index'
import { mockListItems } from './__mocks__/mockListItems'

const stories = storiesOf('ListView', module)
stories.addDecorator(withKnobs)
stories.addDecorator(
defaultTemplate({
title: 'ListView',
documentationLink:
'http://www.patternfly.org/pattern-library/content-views/list-view/'
})
)

const renderActions = () =>
<div>
<Button>Details</Button>
</div>

const renderAdditionalInfoItems = itemProperties => {
return (
itemProperties &&
Object.keys(itemProperties).map(prop => {
const classNames = cx('pficon', {
'pficon-flavor': prop === 'hosts',
'pficon-cluster': prop === 'clusters',
'pficon-container-node': prop === 'nodes',
'pficon-image': prop === 'images'
})
return (
<ListViewInfoItem key={prop}>
<span className={classNames} />
<strong>{itemProperties[prop]}</strong> {prop}
</ListViewInfoItem>
)
})
)
}

stories.addWithInfo(
'List of expandable items',
`ListView usage example.`,
() => {
return (
<ListView>
{mockListItems.map((item, index) =>
<ListViewItem
key={index}
actions={renderActions(item.actions)}
checkboxInput={<input type="checkbox" />}
leftContent={<ListViewIcon icon="fa fa-plane" />}
additionalInfo={renderAdditionalInfoItems(item.properties)}
heading={item.title}
description={item.description}
stacked={boolean('Stacked', false)}
>
<Row>
<Col sm={11}>
{item.expandedContentText}
</Col>
</Row>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like how the ListViewItem knows there is an expansion based solely on if there is a child node rather than requiring an explicit property.

</ListViewItem>
)}
</ListView>
)
}
)

stories.addWithInfo('ListItem variants', `ListView usage example.`, () =>
<ListView
id="listView--listItemVariants"
className="listView--listItemVariants"
>
<ListViewItem
id="item1"
className="listViewItem--listItemVariants"
key="item1"
description="Expandable item with description, additional items and actions"
heading="Event One"
checkboxInput={<input type="checkbox" />}
leftContent={<ListViewIcon icon="fa fa-plane" />}
additionalInfo={[
<ListViewInfoItem key="1">
<span className="pficon pficon-flavor" /> Item 1
</ListViewInfoItem>,
<ListViewInfoItem key="2">
<span className="fa fa-bug" /> Item 2
</ListViewInfoItem>
]}
actions={
<div>
<Button>Action 1</Button>
<DropdownKebab id="action2kebab" pullRight>
<MenuItem>Action 2</MenuItem>
</DropdownKebab>
</div>
}
stacked={boolean('Stacked', false)}
>
Expanded Content
</ListViewItem>
<ListViewItem
key="item2"
leftContent={<ListViewIcon size="lg" icon="fa fa-plane" />}
heading={
<span>
This is EVENT One that is with very LONG and should not overflow and
push other elements out of the bounding box.
<small>Feb 23, 2015 12:32 am</small>
</span>
}
description={
<span>
The following snippet of text is rendered as <a href="">link text</a>.
</span>
}
stacked={boolean('Stacked', false)}
/>
<ListViewItem
key="item3"
checkboxInput={<input type="checkbox" />}
heading="Stacked Additional Info items"
description={
<span>
The following snippet of text is rendered as <a href="">link text</a>.
</span>
}
additionalInfo={[
<ListViewInfoItem key="1" stacked>
<strong>113,735</strong>
<span>Service One</span>
</ListViewInfoItem>,
<ListViewInfoItem key="2" stacked>
<strong>35%</strong>
<span>Service Two</span>
</ListViewInfoItem>
]}
stacked={boolean('Stacked', false)}
/>
<ListViewItem
key="item4"
additionalInfo={[
<ListViewInfoItem key="1">
<span className="pficon pficon-screen" /> Only Additional
</ListViewInfoItem>,
<ListViewInfoItem key="2">
<span className="pficon pficon-cluster" /> Info Items
</ListViewInfoItem>
]}
stacked={boolean('Stacked', false)}
/>
<ListViewItem
key="item5"
heading="Custom Event Icon"
leftContent={
<ListViewIcon
size="md"
icon="pficon pficon-ok list-view-pf-icon-success"
/>
}
description={
<span>
The following snippet of text is rendered as <a href="">link text</a>.
</span>
}
additionalInfo={[
<ListViewInfoItem key="1">
<span className="pficon pficon-screen" />
<strong>108</strong> Hosts
</ListViewInfoItem>,
<ListViewInfoItem key="2">
<span className="pficon pficon-cluster" />
<strong>28</strong> Clusters
</ListViewInfoItem>
]}
stacked={boolean('Stacked', false)}
/>
</ListView>
)
38 changes: 38 additions & 0 deletions src/ListView/ListView.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react'
import renderer from 'react-test-renderer'
import { Row, Col } from 'react-bootstrap'

import { ListView, ListViewItem, ListViewIcon } from '../index'
import {
mockListItems,
renderActions,
renderAdditionalInfoItems
} from './__mocks__/mockListItems'

test('ListView renders properly', () => {
const component = renderer.create(
<ListView>
{mockListItems.map((item, index) =>
<ListViewItem
key={index}
actions={renderActions(item.actions)}
checkboxInput={<input type="checkbox" />}
leftContent={<ListViewIcon icon="fa fa-plane" />}
additionalInfo={renderAdditionalInfoItems(item.properties)}
heading={item.title}
description={item.description}
stacked
>
<Row>
<Col sm={11}>
{item.expandedContentText}
</Col>
</Row>
</ListViewItem>
)}
</ListView>
)

const tree = component.toJSON()
expect(tree).toMatchSnapshot()
})
112 changes: 112 additions & 0 deletions src/ListView/ListViewItem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React from 'react'
import PropTypes from 'prop-types'

import {
ListViewExpand,
ListGroupItem,
ListGroupItemHeader,
ListGroupItemContainer
} from './index'
import ListViewRow from './ListViewRow'

/**
* ListViewItem - main ListViewItem component which handles the expansion logic.
* ListViewItem is considered expandable if it has child props. In that case it
* renders ListGroupItemHeader and ListGroupItemContainer
*/
export default class ListViewItem extends React.Component {
constructor() {
super()
this.state = { expanded: false }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question - do we prefer state object defined inside the constructor or outside within the class? We don't have a linter checking this right now, but it may be good to introduce it at some point for consistency... maybe we can consider more lint settings in a future PR?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All examples in ReactJS doc initialize the state in constructor.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jtomasek I'm good with this pattern (just wanted to note it though). Will update my other PRs accordingly and follow this standard. Thanks!

}

toggleExpanded(index) {
const { onExpand, onExpandClose } = this.props
if (this.state.expanded) {
onExpandClose && onExpandClose()
} else {
onExpand && onExpand()
}
this.setState(prevState => ({ expanded: !prevState.expanded }))
}

render() {
const {
children,
stacked,
actions,
additionalInfo,
description,
heading,
leftContent,
checkboxInput,
...other
} = this.props
const { expanded } = this.state

if (children) {
return (
<ListGroupItem expanded={expanded} stacked={stacked} {...other}>
<ListGroupItemHeader toggleExpanded={() => this.toggleExpanded()}>
<ListViewExpand
expanded={expanded}
toggleExpanded={() => this.toggleExpanded()}
/>
<ListViewRow
checkboxInput={checkboxInput}
leftContent={leftContent}
heading={heading}
description={description}
additionalInfo={additionalInfo}
actions={actions}
/>
</ListGroupItemHeader>
<ListGroupItemContainer
expanded={expanded}
onClose={() => this.toggleExpanded()}
>
{children}
</ListGroupItemContainer>
</ListGroupItem>
)
} else {
return (
<ListGroupItem stacked={stacked} {...other}>
<ListViewRow
checkboxInput={checkboxInput}
leftContent={leftContent}
heading={heading}
description={description}
additionalInfo={additionalInfo}
actions={actions}
/>
</ListGroupItem>
)
}
}
}
ListViewItem.propTypes = {
/** Child node rendered as expanded content of the ListViewItem */
children: PropTypes.node,
/** Display the ListViewItem stacked or not */
stacked: PropTypes.bool.isRequired,
/** Function triggered when expandable content is expanded */
onExpand: PropTypes.func,
/** Function triggered when expandable content is closed */
onExpandClose: PropTypes.func,
/** Node which renders right-positioned actions (e.g. Buttons, DropdownKebab...) */
actions: PropTypes.node,
/** An array of ListViewInfoItem instances to render additional info items */
additionalInfo: PropTypes.arrayOf(PropTypes.node),
/** Contents of ListViewItem description section */
description: PropTypes.node,
/** Contents of ListViewItem heading */
heading: PropTypes.node,
/** Contents for left section of ListViewItem (usually ListViewIcon) */
leftContent: PropTypes.node,
/** Checkbox form input component */
checkboxInput: PropTypes.node
}
ListViewItem.defaultProps = {
stacked: false
}
Loading