(this.refDiv = ref)}
60 | contentEditable="true"
61 | className={this.getClassName()}
62 | onPaste={this.onPaste}
63 | onBlur={this.onBlur}
64 | onInput={this.onTextChange}
65 | onKeyDown={this.onKeyDown}
66 | placeholder={placeholder}
67 | />
68 | )
69 | }
70 | }
71 |
72 | EditableLabel.propTypes = {
73 | onChange: PropTypes.func,
74 | placeholder: PropTypes.string,
75 | autoFocus: PropTypes.bool,
76 | inline: PropTypes.bool,
77 | value: PropTypes.string,
78 | }
79 |
80 | EditableLabel.defaultProps = {
81 | onChange: () => {},
82 | placeholder: '',
83 | autoFocus: false,
84 | inline: false,
85 | value: ''
86 | }
87 | export default EditableLabel
88 |
--------------------------------------------------------------------------------
/src/widgets/InlineInput.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import {InlineInput} from 'rt/styles/Base'
4 | import autosize from 'autosize'
5 |
6 | class InlineInputController extends React.Component {
7 | onFocus = (e) => e.target.select()
8 |
9 | // This is the way to select all text if mouse clicked
10 | onMouseDown = (e) => {
11 | if (document.activeElement != e.target) {
12 | e.preventDefault()
13 | this.refInput.focus()
14 | }
15 | }
16 |
17 | onBlur = () => {
18 | this.updateValue()
19 | }
20 |
21 | onKeyDown = (e) => {
22 | if(e.keyCode == 13) {
23 | this.refInput.blur()
24 | e.preventDefault()
25 | }
26 | if(e.keyCode == 27) {
27 | this.setValue(this.props.value)
28 | this.refInput.blur()
29 | e.preventDefault()
30 | }
31 | if(e.keyCode == 9) {
32 | if (this.getValue().length == 0) {
33 | this.props.onCancel()
34 | }
35 | this.refInput.blur()
36 | e.preventDefault()
37 | }
38 | }
39 |
40 | getValue = () => this.refInput.value
41 | setValue = (value) => this.refInput.value=value
42 |
43 | updateValue = () => {
44 | if (this.getValue() != this.props.value) {
45 | this.props.onSave(this.getValue())
46 | }
47 | }
48 |
49 | setRef = (ref) => {
50 | this.refInput = ref
51 | if (this.props.resize != 'none') {
52 | autosize(this.refInput)
53 | }
54 | }
55 |
56 | UNSAFE_componentWillReceiveProps(nextProps) {
57 | this.setValue(nextProps.value)
58 | }
59 |
60 | render() {
61 | const {autoFocus, border, value, placeholder} = this.props
62 |
63 | return
80 | }
81 | }
82 |
83 | InlineInputController.propTypes = {
84 | onSave: PropTypes.func,
85 | border: PropTypes.bool,
86 | placeholder: PropTypes.string,
87 | value: PropTypes.string,
88 | autoFocus: PropTypes.bool,
89 | resize: PropTypes.oneOf(['none', 'vertical', 'horizontal']),
90 | }
91 |
92 | InlineInputController.defaultProps = {
93 | onSave: () => {},
94 | placeholder: '',
95 | value: '',
96 | border: false,
97 | autoFocus: false,
98 | resize: 'none'
99 | }
100 |
101 | export default InlineInputController
102 |
--------------------------------------------------------------------------------
/src/widgets/NewLaneTitleEditor.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import {InlineInput} from 'rt/styles/Base'
4 | import autosize from 'autosize'
5 |
6 | class NewLaneTitleEditor extends React.Component {
7 | onKeyDown = (e) => {
8 | if(e.keyCode == 13) {
9 | this.refInput.blur()
10 | this.props.onSave()
11 | e.preventDefault()
12 | }
13 | if(e.keyCode == 27) {
14 | this.cancel()
15 | e.preventDefault()
16 | }
17 |
18 | if(e.keyCode == 9) {
19 | if (this.getValue().length == 0) {
20 | this.cancel()
21 | } else {
22 | this.props.onSave()
23 | }
24 | e.preventDefault()
25 | }
26 | }
27 |
28 | cancel = () => {
29 | this.setValue('')
30 | this.props.onCancel()
31 | this.refInput.blur()
32 | }
33 |
34 | getValue = () => this.refInput.value
35 | setValue = (value) => this.refInput.value = value
36 |
37 | saveValue = () => {
38 | if (this.getValue() != this.props.value) {
39 | this.props.onSave(this.getValue())
40 | }
41 | }
42 |
43 | focus = () => this.refInput.focus()
44 |
45 | setRef = (ref) => {
46 | this.refInput = ref
47 | if (this.props.resize != 'none') {
48 | autosize(this.refInput)
49 | }
50 | }
51 |
52 | render() {
53 | const {autoFocus, resize, border, autoResize, value, placeholder} = this.props
54 |
55 | return
66 | }
67 | }
68 |
69 | NewLaneTitleEditor.propTypes = {
70 | onSave: PropTypes.func,
71 | onCancel: PropTypes.func,
72 | border: PropTypes.bool,
73 | placeholder: PropTypes.string,
74 | value: PropTypes.string,
75 | autoFocus: PropTypes.bool,
76 | autoResize: PropTypes.bool,
77 | resize: PropTypes.oneOf(['none', 'vertical', 'horizontal']),
78 | }
79 |
80 | NewLaneTitleEditor.defaultProps = {
81 | inputRef: () => {},
82 | onSave: () => {},
83 | onCancel: () => {},
84 | placeholder: '',
85 | value: '',
86 | border: false,
87 | autoFocus: false,
88 | autoResize: false,
89 | resize: 'none'
90 | }
91 |
92 | export default NewLaneTitleEditor
93 |
--------------------------------------------------------------------------------
/src/widgets/index.js:
--------------------------------------------------------------------------------
1 | import DeleteButton from './DeleteButton'
2 | import EditableLabel from './EditableLabel'
3 | import InlineInput from './InlineInput'
4 |
5 | export default {
6 | DeleteButton,
7 | EditableLabel,
8 | InlineInput
9 | }
10 |
--------------------------------------------------------------------------------
/stories/AsyncLoad.story.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import {storiesOf} from '@storybook/react'
3 |
4 | import Board from '../src'
5 |
6 | const data = require('./data/base.json')
7 |
8 | class AsyncBoard extends Component {
9 | state = {boardData: {lanes: [{id: 'loading', title: 'loading..', cards: []}]}}
10 |
11 | componentDidMount() {
12 | setTimeout(this.getBoard.bind(this), 1000)
13 | }
14 |
15 | getBoard() {
16 | this.setState({boardData: data})
17 | }
18 |
19 | render() {
20 | return
21 | }
22 | }
23 |
24 | storiesOf('Advanced Features', module).add('Async Load data', () =>
, {info: 'Load board data asynchronously after the component has mounted'})
25 |
--------------------------------------------------------------------------------
/stories/Base.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {storiesOf} from '@storybook/react'
3 |
4 | import Board from '../src'
5 |
6 | const data = require('./data/base.json')
7 |
8 | storiesOf('Basic Functions', module).add('Full Board example', () =>
, {
9 | info: 'A complete Trello board with multiple lanes fed as json data'
10 | })
11 |
--------------------------------------------------------------------------------
/stories/CollapsibleLanes.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {storiesOf} from '@storybook/react'
3 | import debug from './helpers/debug'
4 |
5 | import Board from '../src'
6 |
7 | const data = require('./data/collapsible.json')
8 |
9 | storiesOf('Advanced Features', module).add(
10 | 'Collapsible Lanes',
11 | () => {
12 | const shouldReceiveNewData = nextData => {
13 | debug('data has changed')
14 | debug(nextData)
15 | }
16 |
17 | return
18 | },
19 | {info: 'Collapse lanes when double clicking on the lanes'}
20 | )
21 |
--------------------------------------------------------------------------------
/stories/CustomAddCardLink.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {storiesOf} from '@storybook/react'
3 |
4 | import Board from '../src'
5 |
6 | const data = require('./data/collapsible.json')
7 |
8 | const CustomAddCardLink = ({onClick, t}) =>
9 |
10 | storiesOf('Custom Components', module)
11 | .add(
12 | 'AddCardLink',
13 | () => {
14 | return
15 | }
16 | )
17 |
--------------------------------------------------------------------------------
/stories/CustomCard.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {storiesOf} from '@storybook/react'
3 | import {MovableCardWrapper } from 'rt/styles/Base'
4 | import DeleteButton from 'rt/widgets/DeleteButton'
5 |
6 | import Board from '../src'
7 | import Tag from 'rt/components/Card/Tag'
8 |
9 | const CustomCard = ({
10 | onClick,
11 | className,
12 | name,
13 | cardStyle,
14 | body,
15 | dueOn,
16 | cardColor,
17 | subTitle,
18 | tagStyle,
19 | escalationText,
20 | tags,
21 | showDeleteButton,
22 | onDelete,
23 | }) => {
24 | const clickDelete = e => {
25 | onDelete()
26 | e.stopPropagation()
27 | }
28 |
29 | return (
30 |
35 |
45 | {name}
46 | {dueOn}
47 | {showDeleteButton && }
48 |
49 |
50 |
{subTitle}
51 |
52 | {body}
53 |
54 |
{escalationText}
55 | {tags && (
56 |
65 | {tags.map(tag => (
66 |
67 | ))}
68 |
69 | )}
70 |
71 |
72 | )
73 | }
74 |
75 | const data = {
76 | lanes: [
77 | {
78 | id: 'lane1',
79 | title: 'Planned Tasks',
80 | label: '12/12',
81 | style: {backgroundColor: 'cyan', padding: 20},
82 | titleStyle: {fontSize: 20, marginBottom: 15},
83 | labelStyle: {color: '#009688', fontWeight: 'bold'},
84 | cards: [
85 | {
86 | id: 'Card1',
87 | name: 'John Smith',
88 | dueOn: 'due in a day',
89 | subTitle: 'SMS received at 12:13pm today',
90 | body: 'Thanks. Please schedule me for an estimate on Monday.',
91 | escalationText: 'Escalated to OPS-ESCALATIONS!',
92 | cardColor: '#BD3B36',
93 | cardStyle: {borderRadius: 6, boxShadow: '0 0 6px 1px #BD3B36', marginBottom: 15},
94 | metadata: {id: 'Card1'}
95 | },
96 | {
97 | id: 'Card2',
98 | name: 'Card Weathers',
99 | dueOn: 'due now',
100 | subTitle: 'Email received at 1:14pm',
101 | body: 'Is the estimate free, and can someone call me soon?',
102 | escalationText: 'Escalated to Admin',
103 | cardColor: '#E08521',
104 | cardStyle: {borderRadius: 6, boxShadow: '0 0 6px 1px #E08521', marginBottom: 15},
105 | metadata: {id: 'Card1'}
106 | }
107 | ]
108 | },
109 | {
110 | id: 'lane2',
111 | title: 'Long Lane name this is i suppose ha!',
112 | cards: [
113 | {
114 | id: 'Card3',
115 | name: 'Michael Caine',
116 | dueOn: 'due in a day',
117 | subTitle: 'Email received at 4:23pm today',
118 | body: 'You are welcome. Interested in doing business with you' + ' again',
119 | escalationText: 'Escalated to OPS-ESCALATIONS!',
120 | cardColor: '#BD3B36',
121 | cardStyle: {borderRadius: 6, boxShadow: '0 0 6px 1px #BD3B36', marginBottom: 15},
122 | metadata: {id: 'Card1'},
123 | tags: [{title: 'Critical', color: 'white', bgcolor: 'red'}, {title: '2d ETA', color: 'white', bgcolor: '#0079BF'}]
124 | }
125 | ]
126 | }
127 | ]
128 | }
129 |
130 | storiesOf('Custom Components', module).add(
131 | 'Card',
132 | () => {
133 | return (
134 |
alert(`Card with id:${cardId} clicked. Has metadata.id: ${metadata.id}`)}
140 | />
141 | )
142 | },
143 | {info: 'Style your own card appearance. Watch out for usage of tags in custom styling as well!'}
144 | )
145 |
--------------------------------------------------------------------------------
/stories/CustomCardWithDrag.story.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import {storiesOf} from '@storybook/react'
3 | import update from 'immutability-helper'
4 |
5 | import {MovableCardWrapper } from 'rt/styles/Base'
6 | import debug from './helpers/debug'
7 |
8 | import Board from '../src'
9 |
10 | const CustomCard = props => {
11 | return (
12 |
19 |
30 |
31 |
{props.subTitle}
32 |
33 | {props.body}
34 |
35 |
36 |
37 | )
38 | }
39 |
40 | const customCardData = {
41 | lanes: [
42 | {
43 | id: 'lane1',
44 | title: 'Planned',
45 | cards: [
46 | {
47 | id: 'Card1',
48 | name: 'John Smith',
49 | subTitle: 'SMS received at 12:13pm today',
50 | body: 'Thanks. Please schedule me for an estimate on Monday.',
51 | metadata: {id: 'Card1'}
52 | },
53 | {
54 | id: 'Card2',
55 | name: 'Card Weathers',
56 | subTitle: 'Email received at 1:14pm',
57 | body: 'Is the estimate free, and can someone call me soon?',
58 | metadata: {id: 'Card1'}
59 | }
60 | ]
61 | },
62 | {
63 | id: 'lane2',
64 | title: 'Work In Progress',
65 | cards: [
66 | {
67 | id: 'Card3',
68 | name: 'Michael Caine',
69 | subTitle: 'Email received at 4:23pm today',
70 | body: 'You are welcome. Interested in doing business with you again',
71 | metadata: {id: 'Card1'}
72 | }
73 | ]
74 | }
75 | ]
76 | }
77 |
78 | class BoardWithCustomCard extends Component {
79 | state = {boardData: customCardData, draggedData: undefined}
80 |
81 | updateBoard = newData => {
82 | debug('calling updateBoard')
83 | this.setState({draggedData: newData})
84 | }
85 |
86 | onDragEnd = (cardId, sourceLandId, targetLaneId, card) => {
87 | debug('Calling onDragENd')
88 | const {draggedData} = this.state
89 | const laneIndex = draggedData.lanes.findIndex(lane => lane.id === sourceLandId)
90 | const cardIndex = draggedData.lanes[laneIndex].cards.findIndex(card => card.id === cardId)
91 | const updatedData = update(draggedData, {lanes: {[laneIndex]: {cards: {[cardIndex]: {cardColor: {$set: '#d0fdd2'}}}}}})
92 | this.setState({boardData: updatedData})
93 | }
94 |
95 | render() {
96 | return (
97 | alert(`Card with id:${cardId} clicked. Has metadata.id: ${metadata.id}`)}
104 | components={{Card: CustomCard}}
105 | />
106 | )
107 | }
108 | }
109 |
110 | storiesOf('Custom Components', module).add(
111 | 'Drag-n-Drop Styling',
112 | () => {
113 | return
114 | },
115 | {info: 'Change card color on drag-n-drop'}
116 | )
117 |
--------------------------------------------------------------------------------
/stories/CustomLaneFooter.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {storiesOf} from '@storybook/react'
3 |
4 | import Board from '../src'
5 |
6 | const data = require('./data/collapsible.json')
7 |
8 | const LaneFooter = ({onClick, collapsed}) => {collapsed ? 'click to expand' : 'click to collapse'}
9 |
10 | storiesOf('Custom Components', module).
11 | add('LaneFooter', () => )
12 |
--------------------------------------------------------------------------------
/stories/CustomLaneHeader.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {storiesOf} from '@storybook/react'
3 |
4 | import Board from '../src'
5 |
6 | const CustomLaneHeader = ({label, cards, title, current, target}) => {
7 | const buttonHandler = () => {
8 | alert(`The label passed to the lane was: ${label}. The lane has ${cards.length} cards!`)
9 | }
10 | return (
11 |
12 |
21 | {title}
22 | {label && (
23 |
24 |
27 |
28 | )}
29 |
30 |
Percentage: {current || 0}/{target}
31 |
32 | )
33 | }
34 |
35 | storiesOf('Custom Components', module).add(
36 | 'LaneHeader',
37 | () => {
38 | const data = {
39 | lanes: [
40 | {
41 | id: 'lane1',
42 | title: 'Planned Tasks',
43 | current: "70", // custom property
44 | target: "100", // custom property
45 | label: 'First Lane here',
46 | cards: [
47 | {
48 | id: 'Card1',
49 | title: 'John Smith',
50 | description: 'Thanks. Please schedule me for an estimate on Monday.'
51 | },
52 | {
53 | id: 'Card2',
54 | title: 'Card Weathers',
55 | description: 'Email received at 1:14pm'
56 | }
57 | ]
58 | },
59 | {
60 | id: 'lane2',
61 | title: 'Completed Tasks',
62 | label: 'Second Lane here',
63 | current: "30", // custom property
64 | target: "100", // custom property
65 | cards: [
66 | {
67 | id: 'Card3',
68 | title: 'Michael Caine',
69 | description: 'You are welcome. Interested in doing business with you' + ' again',
70 | tags: [{title: 'Critical', color: 'white', bgcolor: 'red'}, {title: '2d ETA', color: 'white', bgcolor: '#0079BF'}]
71 | }
72 | ]
73 | }
74 | ]
75 | }
76 |
77 | return
78 | },
79 | {info: 'Style your lane header appearance'}
80 | )
81 |
--------------------------------------------------------------------------------
/stories/CustomNewCardForm.story.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import {storiesOf} from '@storybook/react'
3 |
4 | import Board from '../src'
5 |
6 | const data = require('./data/base.json')
7 |
8 | class NewCardForm extends Component {
9 | handleAdd = () => this.props.onAdd({title: this.titleRef.value, description: this.descRef.value})
10 | setTitleRef = (ref) => this.titleRef = ref
11 | setDescRef = (ref) => this.descRef = ref
12 | render() {
13 | const {onCancel} = this.props
14 | return (
15 |
16 |
17 |
25 |
26 |
27 |
28 |
29 | )
30 | }
31 | }
32 |
33 | storiesOf('Custom Components', module)
34 | .add(
35 | 'NewCardForm',
36 | () =>
37 | , {info: 'Pass a custom new card form compoment to add card'}
38 | )
39 |
40 |
--------------------------------------------------------------------------------
/stories/CustomNewLaneForm.story.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import {storiesOf} from '@storybook/react'
3 |
4 | import Board from '../src'
5 |
6 | const data = require('./data/data-sort.json')
7 |
8 | class NewLaneForm extends Component {
9 | render () {
10 | const {onCancel, t} = this.props
11 | const handleAdd = () => this.props.onAdd({title: this.inputRef.value})
12 | const setInputRef = (ref) => this.inputRef = ref
13 | return (
14 |
15 |
16 |
17 |
18 |
19 | )
20 | }
21 | }
22 |
23 | storiesOf('Custom Components', module)
24 | .add('NewLaneForm', () => )
25 |
--------------------------------------------------------------------------------
/stories/CustomNewLaneSection.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {storiesOf} from '@storybook/react'
3 |
4 | import Board from '../src'
5 |
6 | const data = require('./data/data-sort.json')
7 |
8 | const NewLaneSection = ({t, onClick}) =>
9 |
10 | storiesOf('Custom Components', module)
11 | .add('NewLaneSection', () => , {
12 | })
13 |
--------------------------------------------------------------------------------
/stories/Deprecations.story.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import {storiesOf} from '@storybook/react'
3 |
4 | import Board from '../src'
5 |
6 | import './board.css'
7 |
8 | const data = require('./data/base.json')
9 |
10 | const CustomLaneHeader = props => {
11 | const buttonHandler = () => {
12 | alert(`The label passed to the lane was: ${props.label}. The lane has ${props.cards.length} cards!`)
13 | }
14 | return (
15 |
16 |
25 | {props.title}
26 | {props.label && (
27 |
28 |
31 |
32 | )}
33 |
34 |
35 | )
36 | }
37 |
38 | class NewCard extends Component {
39 | updateField = (field, evt) => {
40 | this.setState({[field]: evt.target.value})
41 | }
42 |
43 | handleAdd = () => {
44 | this.props.onAdd(this.state)
45 | }
46 |
47 | render() {
48 | const {onCancel} = this.props
49 | return (
50 |
51 |
52 |
60 |
61 |
62 |
63 |
64 | )
65 | }
66 | }
67 | const CustomCard = props => {
68 | return (
69 |
70 |
80 | {props.name}
81 | {props.dueOn}
82 |
83 |
84 |
{props.subTitle}
85 |
86 | {props.body}
87 |
88 |
{props.escalationText}
89 | {props.tags && (
90 |
99 | {props.tags.map(tag => (
100 |
101 | ))}
102 |
103 | )}
104 |
105 |
106 | )
107 | }
108 | storiesOf('Deprecation warnings', module).add(
109 | 'v2.2 warnings',
110 | () => New Card}
114 | customLaneHeader={}
115 | newLaneTemplate={new lane
}
116 | newCardTemplate={}
117 | customCardLayout
118 | >
119 |
120 | ,
121 | {info: 'Example of usage legacy props: addCardLink, customCardLayout, customLaneHeader, newLaneTemplate, newCardTemplate'}
122 | )
123 |
--------------------------------------------------------------------------------
/stories/DragDrop.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {storiesOf} from '@storybook/react'
3 | import debug from './helpers/debug'
4 |
5 | import Board from '../src'
6 |
7 | const data = require('./data/base.json')
8 |
9 | storiesOf('Drag-n-Drop', module)
10 | .add(
11 | 'Basic',
12 | () => {
13 | const handleDragStart = (cardId, laneId) => {
14 | debug('drag started')
15 | debug(`cardId: ${cardId}`)
16 | debug(`laneId: ${laneId}`)
17 | }
18 |
19 | const handleDragEnd = (cardId, sourceLaneId, targetLaneId, position, card) => {
20 | debug('drag ended')
21 | debug(`cardId: ${cardId}`)
22 | debug(`sourceLaneId: ${sourceLaneId}`)
23 | debug(`targetLaneId: ${targetLaneId}`)
24 | debug(`newPosition: ${position}`)
25 | debug(`cardDetails:`)
26 | debug(card)
27 | }
28 |
29 | const handleLaneDragStart = laneId => {
30 | debug(`lane drag started for ${laneId}`)
31 | }
32 |
33 | const handleLaneDragEnd = (removedIndex, addedIndex, {id}) => {
34 | debug(`lane drag ended from position ${removedIndex} for laneId=${id}`)
35 | debug(`New lane position: ${addedIndex}`)
36 | }
37 |
38 | const shouldReceiveNewData = nextData => {
39 | debug('data has changed')
40 | debug(nextData)
41 | }
42 |
43 | const onCardMoveAcrossLanes = (fromLaneId, toLaneId, cardId, addedIndex) => {
44 | debug(`onCardMoveAcrossLanes: ${fromLaneId}, ${toLaneId}, ${cardId}, ${addedIndex}`)
45 | }
46 |
47 | return (
48 |
58 | )
59 | },
60 | {info: 'A demonstration of onDragStart and onDragEnd hooks for card and lanes'}
61 | )
62 | .add(
63 | 'Drag Styling',
64 | () => {
65 | return
66 | },
67 | {info: 'Modifying appearance of dragged card'}
68 | )
69 |
--------------------------------------------------------------------------------
/stories/EditableBoard.story.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import {storiesOf} from '@storybook/react'
3 | import debug from './helpers/debug'
4 |
5 | import Board from '../src'
6 |
7 | const data = require('./data/base.json')
8 | const smallData = require('./data/data-sort')
9 |
10 | const disallowAddingCardData = {...data}
11 | disallowAddingCardData.lanes[0].title = 'Disallowed adding card'
12 | disallowAddingCardData.lanes[0].disallowAddingCard = true
13 |
14 | storiesOf('Editable Board', module)
15 | .add(
16 | 'Add/Delete Cards',
17 | () => {
18 | const shouldReceiveNewData = nextData => {
19 | debug('Board has changed')
20 | debug(nextData)
21 | }
22 |
23 | const handleCardDelete = (cardId, laneId) => {
24 | debug(`Card: ${cardId} deleted from lane: ${laneId}`)
25 | }
26 |
27 | const handleCardAdd = (card, laneId) => {
28 | debug(`New card added to lane ${laneId}`)
29 | debug(card)
30 | }
31 |
32 | return (
33 | alert(`Card with id:${cardId} clicked. Card in lane: ${laneId}`)}
41 | editable
42 | />
43 | )
44 | },
45 | {info: 'Add/delete cards or delete lanes'}
46 | )
47 | .add(
48 | 'Add New Lane',
49 | () => {
50 | return (
51 | debug('You added a line with title ' + t.title)}
56 | />
57 | )
58 | },
59 | {info: 'Allow adding new lane'}
60 | )
61 | .add(
62 | 'Disallow Adding Card for specific Lane',
63 | () => {
64 | return (
65 |
69 | )
70 | },
71 | {info: 'Can hide the add card button on specific lanes'}
72 | )
73 | .add(
74 | 'Inline Edit Lane Title and Cards',
75 | () => {
76 | return (
77 | debug(`onCardUpdate: ${cardId} -> ${JSON.stringify(data, null, 2)}`)}
83 | onLaneUpdate={ (laneId, data) => debug(`onLaneUpdate: ${laneId} -> ${data.title}`)}
84 | onLaneAdd={t => debug('You added a line with title ' + t.title)}
85 | />
86 | )
87 | },
88 | {info: 'Allow edit lane title and cards'}
89 | )
90 |
--------------------------------------------------------------------------------
/stories/I18n.story.js:
--------------------------------------------------------------------------------
1 | import React, {Component, Suspense} from 'react'
2 | import {storiesOf} from '@storybook/react'
3 | import { useTranslation, I18nextProvider } from 'react-i18next';
4 |
5 | import Board from '../src'
6 | import i18n from './helpers/i18n'
7 | import createTranslate from 'rt/helpers/createTranslate'
8 |
9 | const smallData = require('./data/data-sort')
10 |
11 | const I18nBoard = () => {
12 | const { t } = useTranslation()
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | )
22 | }
23 |
24 | storiesOf('I18n', module)
25 | .add(
26 | 'Custom texts',
27 | () => {
28 |
29 | const TEXTS = {
30 | "Add another lane": "NEW LANE",
31 | "Click to add card": "Click to add card",
32 | "Delete lane": "Delete lane",
33 | "Lane actions": "Lane actions",
34 | "button": {
35 | "Add lane": "Add lane",
36 | "Add card": "Add card",
37 | "Cancel": "Cancel"
38 | },
39 | "placeholder": {
40 | "title": "title",
41 | "description": "description",
42 | "label": "label"
43 | }
44 | }
45 |
46 | const customTranslation = createTranslate(TEXTS)
47 | return
48 | },
49 | {info: 'Have custom text titles'}
50 | )
51 | .add(
52 | 'Flat translation table',
53 | () => {
54 | const FLAT_TRANSLATION_TABLE = {
55 | "Add another lane": "+ Weitere Liste erstellen",
56 | "Click to add card": "Klicken zum Erstellen einer Karte",
57 | "Delete lane": "Liste löschen",
58 | "Lane actions": "Listenaktionen",
59 | "button.Add lane": "Liste hinzufügen",
60 | "button.Add card": "Karte hinzufügen",
61 | "button.Cancel": "Abbrechen",
62 | "placeholder.title": "Titel",
63 | "placeholder.description": "Beschreibung",
64 | "placeholder.label": "Label"
65 | };
66 |
67 | return FLAT_TRANSLATION_TABLE[key]} editable canAddLanes draggable />
68 | },
69 | {info: 'Flat translation table'}
70 | )
71 |
72 | storiesOf('I18n', module)
73 | .addDecorator(story => {story()})
74 | .add(
75 | 'Using i18next',
76 | () => ,
77 | {info: 'Availability to switching between languages'}
78 | )
79 |
--------------------------------------------------------------------------------
/stories/Interactions.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {storiesOf} from '@storybook/react'
3 |
4 | import Board from '../src'
5 |
6 | const data = {
7 | lanes: [
8 | {
9 | id: 'lane1',
10 | title: 'Planned Tasks',
11 | cards: [
12 | {id: 'Card1', title: 'Card1', description: 'foo card', metadata: {id: 'Card1'}},
13 | {id: 'Card2', title: 'Card2', description: 'bar card', metadata: {id: 'Card2'}}
14 | ]
15 | },
16 | {
17 | id: 'lane2',
18 | title: 'Executing',
19 | cards: [{id: 'Card3', title: 'Card3', description: 'foobar card', metadata: {id: 'Card3'}}]
20 | }
21 | ]
22 | }
23 |
24 | storiesOf('Advanced Features', module).add(
25 | 'Event Handling',
26 | () => (
27 | alert(`Card with id:${cardId} clicked. Has metadata.id: ${metadata.id}. Card in lane: ${laneId}`)}
31 | onLaneClick={laneId => alert(`Lane with id:${laneId} clicked`)}
32 | />
33 | ),
34 | {info: 'Adding event handlers to cards'}
35 | )
36 |
--------------------------------------------------------------------------------
/stories/MultipleBoards.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {storiesOf} from '@storybook/react'
3 |
4 | import Board from '../src'
5 |
6 | const data1 = require('./data/base.json')
7 | const data2 = require('./data/other-board')
8 |
9 | const containerStyles = {
10 | height: 500,
11 | padding: 20
12 | }
13 |
14 | storiesOf('Multiple Boards', module).add(
15 | 'Two Boards',
16 | () => {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | )
27 | },
28 | {info: 'Have two boards rendering their own data'}
29 | )
30 |
--------------------------------------------------------------------------------
/stories/Pagination.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {storiesOf} from '@storybook/react'
3 |
4 | import Board from '../src'
5 |
6 | storiesOf('Basic Functions', module).add(
7 | 'Infinite Scrolling',
8 | () => {
9 | const PER_PAGE = 15
10 |
11 | function delayedPromise(durationInMs, resolutionPayload) {
12 | return new Promise(function(resolve) {
13 | setTimeout(function() {
14 | resolve(resolutionPayload)
15 | }, durationInMs)
16 | })
17 | }
18 |
19 | function generateCards(requestedPage = 1) {
20 | const cards = []
21 | let fetchedItems = (requestedPage - 1) * PER_PAGE
22 | for (let i = fetchedItems + 1; i <= fetchedItems + PER_PAGE; i++) {
23 | cards.push({
24 | id: `${i}`,
25 | title: `Card${i}`,
26 | description: `Description for #${i}`
27 | })
28 | }
29 | return cards
30 | }
31 |
32 | function paginate(requestedPage, laneId) {
33 | // simulate no more cards after page 2
34 | if (requestedPage > 2) {
35 | return delayedPromise(2000, [])
36 | }
37 | let newCards = generateCards(requestedPage)
38 | return delayedPromise(2000, newCards)
39 | }
40 |
41 | const data = {
42 | lanes: [
43 | {
44 | id: 'Lane1',
45 | title: 'Lane1',
46 | cards: generateCards()
47 | }
48 | ]
49 | }
50 |
51 | return parseInt(card1.id) - parseInt(card2.id)} onLaneScroll={paginate} />
52 | },
53 | {
54 | info: `
55 | Infinite scroll with onLaneScroll function callback to fetch more items
56 |
57 | The callback function passed to onLaneScroll will be of the following form
58 | ~~~js
59 | function paginate(requestedPage, laneId) {
60 | return fetchCardsFromBackend(laneId, requestedPage);
61 | };
62 | ~~~
63 | `
64 | }
65 | )
66 |
--------------------------------------------------------------------------------
/stories/PaginationAndEvents.story.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import {storiesOf} from '@storybook/react'
3 |
4 | import Board from '../src'
5 |
6 | let eventBus
7 |
8 | const PER_PAGE = 15
9 |
10 | const addCard = () => {
11 | eventBus.publish({
12 | type: 'ADD_CARD',
13 | laneId: 'Lane1',
14 | card: {id: '000', title: 'EC2 Instance Down', label: '30 mins', description: 'Main EC2 instance down', metadata: {cardId: '000'}}
15 | })
16 | }
17 |
18 | function generateCards(requestedPage = 1) {
19 | const cards = []
20 | let fetchedItems = (requestedPage - 1) * PER_PAGE
21 | for (let i = fetchedItems + 1; i <= fetchedItems + PER_PAGE; i++) {
22 | cards.push({
23 | id: `${i}`,
24 | title: `Card${i}`,
25 | description: `Description for #${i}`,
26 | metadata: {cardId: `${i}`}
27 | })
28 | }
29 | return cards
30 | }
31 |
32 | class BoardWrapper extends Component {
33 | state = {data: this.props.data}
34 |
35 | setEventBus = handle => {
36 | eventBus = handle
37 | }
38 |
39 | delayedPromise = (durationInMs, resolutionPayload) => {
40 | return new Promise(function(resolve) {
41 | setTimeout(function() {
42 | resolve(resolutionPayload)
43 | }, durationInMs)
44 | })
45 | }
46 |
47 | refreshCards = () => {
48 | eventBus.publish({
49 | type: 'REFRESH_BOARD',
50 | data: {
51 | lanes: [
52 | {
53 | id: 'Lane1',
54 | title: 'Changed Lane',
55 | cards: []
56 | }
57 | ]
58 | }
59 | })
60 | }
61 |
62 | paginate = (requestedPage, laneId) => {
63 | let newCards = generateCards(requestedPage)
64 | return this.delayedPromise(2000, newCards)
65 | }
66 |
67 | render() {
68 | return (
69 |
70 |
73 |
76 | parseInt(card1.id) - parseInt(card2.id)}
80 | onLaneScroll={this.paginate}
81 | />
82 |
83 | )
84 | }
85 | }
86 |
87 | storiesOf('Advanced Features', module).add(
88 | 'Scrolling and Events',
89 | () => {
90 | const data = {
91 | lanes: [
92 | {
93 | id: 'Lane1',
94 | title: 'Lane1',
95 | cards: generateCards()
96 | }
97 | ]
98 | }
99 |
100 | return
101 | },
102 | {
103 | info: `
104 | Infinite scroll with onLaneScroll function callback to fetch more items
105 |
106 | The callback function passed to onLaneScroll will be of the following form
107 | ~~~js
108 | function paginate(requestedPage, laneId) {
109 | return fetchCardsFromBackend(laneId, requestedPage);
110 | };
111 | ~~~
112 | `
113 | }
114 | )
115 |
--------------------------------------------------------------------------------
/stories/Realtime.story.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import {storiesOf} from '@storybook/react'
3 | import update from 'immutability-helper'
4 | import debug from './helpers/debug'
5 |
6 | import Board from '../src'
7 |
8 | const data = require('./data/base.json')
9 |
10 | class RealtimeBoard extends Component {
11 | state = {boardData: data, eventBus: undefined}
12 |
13 | setEventBus = handle => {
14 | this.state.eventBus = handle
15 | }
16 |
17 | completeMilkEvent = () => {
18 | this.state.eventBus.publish({type: 'REMOVE_CARD', laneId: 'PLANNED', cardId: 'Milk'})
19 | this.state.eventBus.publish({
20 | type: 'ADD_CARD',
21 | laneId: 'COMPLETED',
22 | card: {id: 'Milk', title: 'Buy Milk', label: '15 mins', description: 'Use Headspace app'}
23 | })
24 | }
25 |
26 | addBlockedEvent = () => {
27 | this.state.eventBus.publish({
28 | type: 'ADD_CARD',
29 | laneId: 'BLOCKED',
30 | card: {id: 'Ec2Error', title: 'EC2 Instance Down', label: '30 mins', description: 'Main EC2 instance down'}
31 | })
32 | }
33 |
34 | modifyLaneTitle = () => {
35 | const data = this.state.boardData
36 | const newData = update(data, {lanes: {1: {title: {$set: 'New Lane Title'}}}})
37 | this.setState({boardData: newData})
38 | }
39 |
40 | modifyCardTitle = () => {
41 | const data = this.state.boardData
42 | const newData = update(data, {lanes: {1: {cards: {0: {title: {$set: 'New Card Title'}}}}}})
43 | this.setState({boardData: newData})
44 | }
45 |
46 | updateCard = () => {
47 | this.state.eventBus.publish({
48 | type: 'UPDATE_CARD',
49 | laneId: 'PLANNED',
50 | card: {id: 'Plan2', title: 'UPDATED Dispose Garbage', label: '45 mins', description: 'UPDATED Sort out recyclable and waste as needed'}
51 | })
52 | }
53 |
54 | prioritizeWriteBlog = () => {
55 | this.state.eventBus.publish({
56 | type: 'MOVE_CARD',
57 | fromLaneId: 'PLANNED',
58 | toLaneId: 'WIP',
59 | cardId: 'Plan3',
60 | index: 0
61 | })
62 | }
63 |
64 | shouldReceiveNewData = nextData => {
65 | debug('data has changed')
66 | debug(nextData)
67 | }
68 |
69 | render() {
70 | return (
71 |
72 |
75 |
78 |
81 |
84 |
87 |
90 |
91 |
92 | )
93 | }
94 | }
95 |
96 | storiesOf('Advanced Features', module).add('Realtime Events', () => , {
97 | info: 'This is an illustration of external events that modify the cards in the board'
98 | })
99 |
--------------------------------------------------------------------------------
/stories/RestrictedLanes.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {storiesOf} from '@storybook/react'
3 |
4 | import Board from '../src'
5 |
6 | const data = require('./data/drag-drop.json')
7 |
8 | storiesOf('Drag-n-Drop', module).add(
9 | 'Restrict lanes',
10 | () => {
11 | return
12 | },
13 | {info: 'Use droppable property to prevent some lanes from being droppable'}
14 | )
15 |
16 | storiesOf('Drag-n-Drop', module).add(
17 | 'Drag Cards not Lanes',
18 | () => {
19 | return
20 | },
21 | {info: 'Use props to disable dragging lanes but enable card dragging'}
22 | )
23 |
--------------------------------------------------------------------------------
/stories/Sort.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {storiesOf} from '@storybook/react'
3 |
4 | import Board from '../src'
5 |
6 | const data = require('./data/data-sort.json')
7 |
8 | storiesOf('Basic Functions', module)
9 | .add(
10 | 'Sorted Lane',
11 | () => new Date(card1.metadata.completedAt) - new Date(card2.metadata.completedAt)} />,
12 | {info: 'A lane sorted by completed at ascending'}
13 | )
14 | .add(
15 | 'Reverse Sorted Lane',
16 | () => new Date(card2.metadata.completedAt) - new Date(card1.metadata.completedAt)} />,
17 | {info: 'A lane sorted by completed at descending'}
18 | )
19 |
--------------------------------------------------------------------------------
/stories/Styling.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {storiesOf} from '@storybook/react'
3 |
4 | import Board from '../src'
5 |
6 | import './board.css'
7 |
8 | const data = require('./data/base.json')
9 |
10 | storiesOf('Styling', module).add(
11 | 'Board Styling',
12 | () => ,
13 | {info: 'Change the background and other css styles for the board container'}
14 | )
15 |
16 | const dataWithLaneStyles = {
17 | lanes: [
18 | {
19 | id: 'PLANNED',
20 | title: 'Planned Tasks',
21 | label: '20/70',
22 | style: {width: 280, backgroundColor: '#3179ba', color: '#fff', boxShadow: '2px 2px 4px 0px rgba(0,0,0,0.75)'},
23 | cards: [
24 | {
25 | id: 'Milk',
26 | title: 'Buy milk',
27 | label: '15 mins',
28 | description: '2 Gallons of milk at the Deli store',
29 | },
30 | {
31 | id: 'Plan2',
32 | title: 'Dispose Garbage',
33 | label: '10 mins',
34 | description: 'Sort out recyclable and waste as needed'
35 | }
36 | ]
37 | },
38 | {
39 | id: 'DONE',
40 | title: 'Doned tasks',
41 | label: '10/70',
42 | style: {width: 280, backgroundColor: '#ba7931', color: '#fff', boxShadow: '2px 2px 4px 0px rgba(0,0,0,0.75)'},
43 | cards: [
44 | {
45 | id: 'burn',
46 | title: 'Burn Garbage',
47 | label: '10 mins',
48 | description: 'Sort out recyclable and waste as needed'
49 | },
50 | ]
51 | },
52 | {
53 | id: 'ARCHIVE',
54 | title: 'Archived tasks',
55 | label: '1/2',
56 | cards: [
57 | {
58 | id: 'archived',
59 | title: 'Archived',
60 | label: '10 mins',
61 | },
62 | ]
63 | }
64 | ]
65 | }
66 |
67 | storiesOf('Styling', module)
68 | .add('Lane Styling',
69 | () => ,
70 | {
71 | info: 'Change the look and feel of the lane'
72 | })
73 |
74 | const dataWithCardStyles = {
75 | lanes: [
76 | {
77 | id: 'PLANNED',
78 | title: 'Planned Tasks',
79 | label: '20/70',
80 | cards: [
81 | {
82 | id: 'Milk',
83 | title: 'Buy milk',
84 | label: '15 mins',
85 | description: '2 Gallons of milk at the Deli store',
86 | style: { backgroundColor: '#eec' },
87 | },
88 | {
89 | id: 'Plan2',
90 | title: 'Dispose Garbage',
91 | label: '10 mins',
92 | description: 'Sort out recyclable and waste as needed'
93 | },
94 | {
95 | id: 'Plan3',
96 | title: 'Burn Garbage',
97 | label: '20 mins'
98 | }
99 | ]
100 | }
101 | ]
102 | }
103 |
104 | storiesOf('Styling', module).add('Card Styling', () => , {
105 | info: 'Change the background of cards'
106 | })
107 |
--------------------------------------------------------------------------------
/stories/Tags.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {storiesOf} from '@storybook/react'
3 |
4 | import Board from '../src'
5 |
6 | storiesOf('Basic Functions', module).add(
7 | 'Tags',
8 | () => {
9 | const data = {
10 | lanes: [
11 | {
12 | id: 'lane1',
13 | title: 'Planned Tasks',
14 | cards: [
15 | {
16 | id: 'Card1',
17 | title: 'Card1',
18 | description: 'foo card',
19 | metadata: {cardId: 'Card1'},
20 | tags: [
21 | {title: 'High', color: 'white', bgcolor: '#EB5A46'},
22 | {title: 'Tech Debt', color: 'white', bgcolor: '#0079BF'},
23 | {title: 'Very long tag that is', color: 'white', bgcolor: '#61BD4F'},
24 | {title: 'One more', color: 'white', bgcolor: '#61BD4F'}
25 | ]
26 | },
27 | {id: 'Card2', title: 'Card2', description: 'bar card', metadata: {cardId: 'Card2'}, tags: [{title: 'Low'}]}
28 | ]
29 | }
30 | ]
31 | }
32 | return
33 | },
34 | {info: 'Customizable tags for each card'}
35 | )
36 |
--------------------------------------------------------------------------------
/stories/board.css:
--------------------------------------------------------------------------------
1 | .boardContainer {
2 | background-color: #4BBF6B;
3 | }
--------------------------------------------------------------------------------
/stories/data/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "lanes": [
3 | {
4 | "id": "PLANNED",
5 | "title": "Planned Tasks",
6 | "label": "20/70",
7 | "style": {"width": 280},
8 | "cards": [
9 | {
10 | "id": "Milk",
11 | "title": "Buy milk",
12 | "label": "15 mins",
13 | "description": "2 Gallons of milk at the Deli store"
14 | },
15 | {
16 | "id": "Plan2",
17 | "title": "Dispose Garbage",
18 | "label": "10 mins",
19 | "description": "Sort out recyclable and waste as needed"
20 | },
21 | {
22 | "id": "Plan3",
23 | "title": "Write Blog",
24 | "label": "30 mins",
25 | "description": "Can AI make memes?"
26 | },
27 | {
28 | "id": "Plan4",
29 | "title": "Pay Rent",
30 | "label": "5 mins",
31 | "description": "Transfer to bank account"
32 | }
33 | ]
34 | },
35 | {
36 | "id": "WIP",
37 | "title": "Work In Progress",
38 | "label": "10/20",
39 | "style": {"width": 280},
40 | "cards": [
41 | {
42 | "id": "Wip1",
43 | "title": "Clean House",
44 | "label": "30 mins",
45 | "description": "Soap wash and polish floor. Polish windows and doors. Scrap all broken glasses"
46 | }
47 | ]
48 | },
49 | {
50 | "id": "BLOCKED",
51 | "title": "Blocked",
52 | "label": "0/0",
53 | "style": {"width": 280},
54 | "cards": []
55 | },
56 | {
57 | "id": "COMPLETED",
58 | "title": "Completed",
59 | "style": {"width": 280},
60 | "label": "2/5",
61 | "cards": [
62 | {
63 | "id": "Completed1",
64 | "title": "Practice Meditation",
65 | "label": "15 mins",
66 | "description": "Use Headspace app"
67 | },
68 | {
69 | "id": "Completed2",
70 | "title": "Maintain Daily Journal",
71 | "label": "15 mins",
72 | "description": "Use Spreadsheet for now"
73 | }
74 | ]
75 | },
76 | {
77 | "id": "REPEAT",
78 | "title": "Repeat",
79 | "style": {"width": 280},
80 | "label": "1/1",
81 | "cards": [
82 | {
83 | "id": "Repeat1",
84 | "title": "Morning Jog",
85 | "label": "30 mins",
86 | "description": "Track using fitbit"
87 | }
88 | ]
89 | },
90 | {
91 | "id": "ARCHIVED",
92 | "title": "Archived",
93 | "style": {"width": 280},
94 | "label": "1/1",
95 | "cards": [
96 | {
97 | "id": "Archived1",
98 | "title": "Go Trekking",
99 | "label": "300 mins",
100 | "description": "Completed 10km on cycle"
101 | }
102 | ]
103 | },
104 | {
105 | "id": "ARCHIVED2",
106 | "title": "Archived2",
107 | "style": {"width": 280},
108 | "label": "1/1",
109 | "cards": [
110 | {
111 | "id": "Archived2",
112 | "title": "Go Jogging",
113 | "label": "300 mins",
114 | "description": "Completed 10km on cycle"
115 | }
116 | ]
117 | },
118 | {
119 | "id": "ARCHIVED3",
120 | "title": "Archived3",
121 | "style": {"width": 280},
122 | "label": "1/1",
123 | "cards": [
124 | {
125 | "id": "Archived3",
126 | "title": "Go Cycling",
127 | "label": "300 mins",
128 | "description": "Completed 10km on cycle"
129 | }
130 | ]
131 | }
132 | ]
133 | }
134 |
--------------------------------------------------------------------------------
/stories/data/board_with_custom_width.json:
--------------------------------------------------------------------------------
1 | {
2 | "lanes": [
3 | {
4 | "id": "PLANNED",
5 | "title": "Planned Tasks",
6 | "label": "20/70",
7 | "style": {"width": 280},
8 | "cards": [
9 | {
10 | "id": "Milk",
11 | "title": "Buy milk",
12 | "label": "15 mins",
13 | "cardStyle": { "width": 270, "maxWidth": 270, "margin": "auto", "marginBottom": 5 },
14 | "description": "2 Gallons of milk at the Deli store"
15 | },
16 | {
17 | "id": "Plan2",
18 | "title": "Dispose Garbage",
19 | "label": "10 mins",
20 | "cardStyle": { "width": 270, "maxWidth": 270, "margin": "auto", "marginBottom": 5 },
21 | "description": "Sort out recyclable and waste as needed"
22 | },
23 | {
24 | "id": "Plan3",
25 | "title": "Write Blog",
26 | "label": "30 mins",
27 | "cardStyle": { "width": 270, "maxWidth": 270, "margin": "auto", "marginBottom": 5 },
28 | "description": "Can AI make memes?"
29 | },
30 | {
31 | "id": "Plan4",
32 | "title": "Pay Rent",
33 | "label": "5 mins",
34 | "cardStyle": { "width": 270, "maxWidth": 270, "margin": "auto", "marginBottom": 5 },
35 | "description": "Transfer to bank account"
36 | }
37 | ]
38 | },
39 | {
40 | "id": "WIP",
41 | "title": "Work In Progress",
42 | "label": "10/20",
43 | "style": {"width": 280},
44 | "cards": [
45 | {
46 | "id": "Wip1",
47 | "title": "Clean House",
48 | "label": "30 mins",
49 | "cardStyle": { "width": 270, "maxWidth": 270, "margin": "auto", "marginBottom": 5 },
50 | "description": "Soap wash and polish floor. Polish windows and doors. Scrap all broken glasses"
51 | }
52 | ]
53 | },
54 | {
55 | "id": "BLOCKED",
56 | "title": "Blocked",
57 | "label": "0/0",
58 | "style": {"width": 280},
59 | "cards": []
60 | },
61 | {
62 | "id": "COMPLETED",
63 | "title": "Completed",
64 | "style": {"width": 280},
65 | "label": "2/5",
66 | "cards": [
67 | {
68 | "id": "Completed1",
69 | "title": "Practice Meditation",
70 | "label": "15 mins",
71 | "cardStyle": { "width": 270, "maxWidth": 270, "margin": "auto", "marginBottom": 5 },
72 | "description": "Use Headspace app"
73 | },
74 | {
75 | "id": "Completed2",
76 | "title": "Maintain Daily Journal",
77 | "label": "15 mins",
78 | "cardStyle": { "width": 270, "maxWidth": 270, "margin": "auto", "marginBottom": 5 },
79 | "description": "Use Spreadsheet for now"
80 | }
81 | ]
82 | },
83 | {
84 | "id": "REPEAT",
85 | "title": "Repeat",
86 | "style": {"width": 280},
87 | "label": "1/1",
88 | "cards": [
89 | {
90 | "id": "Repeat1",
91 | "title": "Morning Jog",
92 | "label": "30 mins",
93 | "cardStyle": { "width": 270, "maxWidth": 270, "margin": "auto", "marginBottom": 5 },
94 | "description": "Track using fitbit"
95 | }
96 | ]
97 | },
98 | {
99 | "id": "ARCHIVED",
100 | "title": "Archived",
101 | "style": {"width": 280},
102 | "label": "1/1",
103 | "cards": [
104 | {
105 | "id": "Archived1",
106 | "title": "Go Trekking",
107 | "label": "300 mins",
108 | "cardStyle": { "width": 270, "maxWidth": 270, "margin": "auto", "marginBottom": 5 },
109 | "description": "Completed 10km on cycle"
110 | }
111 | ]
112 | },
113 | {
114 | "id": "ARCHIVED2",
115 | "title": "Archived2",
116 | "style": {"width": 280},
117 | "label": "1/1",
118 | "cards": [
119 | {
120 | "id": "Archived1",
121 | "title": "Go Trekking",
122 | "label": "300 mins",
123 | "cardStyle": { "width": 270, "maxWidth": 270, "margin": "auto", "marginBottom": 5 },
124 | "description": "Completed 10km on cycle"
125 | }
126 | ]
127 | },
128 | {
129 | "id": "ARCHIVED3",
130 | "title": "Archived3",
131 | "style": {"width": 280},
132 | "label": "1/1",
133 | "cards": [
134 | {
135 | "id": "Archived1",
136 | "title": "Go Trekking",
137 | "label": "300 mins",
138 | "cardStyle": { "width": 270, "maxWidth": 270, "margin": "auto", "marginBottom": 5 },
139 | "description": "Completed 10km on cycle"
140 | }
141 | ]
142 | }
143 | ]
144 | }
145 |
--------------------------------------------------------------------------------
/stories/data/collapsible.json:
--------------------------------------------------------------------------------
1 | {
2 | "lanes": [
3 | {
4 | "id": "PLANNED",
5 | "title": "Double Click Here",
6 | "label": "20/70",
7 | "style": {"width": 280},
8 | "cards": [
9 | {
10 | "id": "Milk",
11 | "title": "Buy milk",
12 | "label": "15 mins",
13 | "description": "2 Gallons of milk at the Deli store"
14 | },
15 | {
16 | "id": "Plan2",
17 | "title": "Dispose Garbage",
18 | "label": "10 mins",
19 | "description": "Sort out recyclable and waste as needed"
20 | },
21 | {
22 | "id": "Plan3",
23 | "title": "Write Blog",
24 | "label": "30 mins",
25 | "description": "Can AI make memes?"
26 | },
27 | {
28 | "id": "Plan4",
29 | "title": "Pay Rent",
30 | "label": "5 mins",
31 | "description": "Transfer to bank account"
32 | }
33 | ]
34 | },
35 | {
36 | "id": "WIP",
37 | "title": "Work In Progress",
38 | "label": "10/20",
39 | "style": {"width": 280},
40 | "cards": [
41 | {
42 | "id": "Wip1",
43 | "title": "Clean House",
44 | "label": "30 mins",
45 | "description": "Soap wash and polish floor. Polish windows and doors. Scrap all broken glasses"
46 | }
47 | ]
48 | },
49 | {
50 | "id": "BLOCKED",
51 | "title": "Blocked",
52 | "label": "0/0",
53 | "style": {"width": 280},
54 | "cards": []
55 | },
56 | {
57 | "id": "COMPLETED",
58 | "title": "Completed",
59 | "style": {"width": 280},
60 | "label": "2/5",
61 | "cards": [
62 | {
63 | "id": "Completed1",
64 | "title": "Practice Meditation",
65 | "label": "15 mins",
66 | "description": "Use Headspace app"
67 | },
68 | {
69 | "id": "Completed2",
70 | "title": "Maintain Daily Journal",
71 | "label": "15 mins",
72 | "description": "Use Spreadsheet for now"
73 | }
74 | ]
75 | },
76 | {
77 | "id": "REPEAT",
78 | "title": "Repeat",
79 | "style": {"width": 280},
80 | "label": "1/1",
81 | "cards": [
82 | {
83 | "id": "Repeat1",
84 | "title": "Morning Jog",
85 | "label": "30 mins",
86 | "description": "Track using fitbit"
87 | }
88 | ]
89 | },
90 | {
91 | "id": "ARCHIVED",
92 | "title": "Archived",
93 | "style": {"width": 280},
94 | "label": "1/1",
95 | "cards": [
96 | {
97 | "id": "Archived1",
98 | "title": "Go Trekking",
99 | "label": "300 mins",
100 | "description": "Completed 10km on cycle"
101 | }
102 | ]
103 | },
104 | {
105 | "id": "ARCHIVED2",
106 | "title": "Archived2",
107 | "style": {"width": 280},
108 | "label": "1/1",
109 | "cards": [
110 | {
111 | "id": "Archived2",
112 | "title": "Go Jogging",
113 | "label": "300 mins",
114 | "description": "Completed 10km on cycle"
115 | }
116 | ]
117 | },
118 | {
119 | "id": "ARCHIVED3",
120 | "title": "Archived3",
121 | "style": {"width": 280},
122 | "label": "1/1",
123 | "cards": [
124 | {
125 | "id": "Archived3",
126 | "title": "Go Cycling",
127 | "label": "300 mins",
128 | "description": "Completed 10km on cycle"
129 | }
130 | ]
131 | }
132 | ]
133 | }
134 |
--------------------------------------------------------------------------------
/stories/data/data-sort.json:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "lanes": [
4 | {
5 | "id": "SORTED_LANE",
6 | "title": "Sorted Lane",
7 | "label": "20/70",
8 | "cards": [
9 | {
10 | "id": "Card1",
11 | "title": "Buy milk",
12 | "label": "2017-12-01",
13 | "description": "2 Gallons of milk at the Deli store",
14 | "metadata": {
15 | "completedAt": "2017-12-01T10:00:00Z",
16 | "shortCode": "abc"
17 | }
18 | },
19 | {
20 | "id": "Card2",
21 | "title": "Dispose Garbage",
22 | "label": "2017-11-01",
23 | "description": "Sort out recyclable and waste as needed",
24 | "metadata": {
25 | "completedAt": "2017-11-01T10:00:00Z",
26 | "shortCode": "aaa"
27 | }
28 | },
29 | {
30 | "id": "Card3",
31 | "title": "Write Blog",
32 | "label": "2017-10-01",
33 | "description": "Can AI make memes?",
34 | "metadata": {
35 | "completedAt": "2017-10-01T10:00:00Z",
36 | "shortCode": "fa1"
37 | }
38 | },
39 | {
40 | "id": "Card4",
41 | "title": "Pay Rent",
42 | "label": "2017-09-01",
43 | "description": "Transfer to bank account",
44 | "metadata": {
45 | "completedAt": "2017-09-01T10:00:00Z",
46 | "shortCode": "ga2"
47 | }
48 | }
49 | ]
50 | }
51 | ]
52 | }
53 |
--------------------------------------------------------------------------------
/stories/data/drag-drop.json:
--------------------------------------------------------------------------------
1 | {
2 | "lanes": [
3 | {
4 | "id": "PLANNED",
5 | "title": "Planned Tasks",
6 | "label": "20/70",
7 | "cards": [
8 | {
9 | "id": "Milk",
10 | "title": "Buy milk",
11 | "label": "15 mins",
12 | "description": "2 Gallons of milk at the Deli store"
13 | },
14 | {
15 | "id": "Plan2",
16 | "title": "Dispose Garbage",
17 | "label": "10 mins",
18 | "description": "Sort out recyclable and waste as needed"
19 | },
20 | {
21 | "id": "Plan3",
22 | "title": "Write Blog",
23 | "label": "30 mins",
24 | "description": "Can AI make memes?"
25 | },
26 | {
27 | "id": "Plan4",
28 | "title": "Pay Rent",
29 | "label": "5 mins",
30 | "description": "Transfer to bank account"
31 | }
32 | ]
33 | },
34 | {
35 | "id": "WIP",
36 | "title": "Work In Progress (Not Droppable)",
37 | "label": "10/20",
38 | "droppable": false,
39 | "cards": [
40 | {
41 | "id": "Wip1",
42 | "title": "Clean House",
43 | "label": "30 mins",
44 | "description": "Soap wash and polish floor. Polish windows and doors. Scrap all broken glasses"
45 | }
46 | ]
47 | },
48 | {
49 | "id": "COMPLETED",
50 | "title": "Completed (Droppable)",
51 | "label": "0/0",
52 | "style": {"width": 280},
53 | "cards": []
54 | }
55 | ]
56 | }
--------------------------------------------------------------------------------
/stories/data/other-board.json:
--------------------------------------------------------------------------------
1 | {
2 | "lanes": [
3 | {
4 | "id": "yesterday",
5 | "title": "Yesterday",
6 | "label": "20/70",
7 | "cards": [
8 | {
9 | "id": "Wip1",
10 | "title": "Clean House",
11 | "label": "30 mins",
12 | "description": "Soap wash and polish floor. Polish windows and doors. Scrap all broken glasses"
13 | }
14 | ]
15 | },
16 | {
17 | "id": "today",
18 | "title": "Today",
19 | "label": "10/20",
20 | "droppable": false,
21 | "cards": [
22 | {
23 | "id": "Milk",
24 | "title": "Buy milk",
25 | "label": "15 mins",
26 | "description": "2 Gallons of milk at the Deli store"
27 | },
28 | {
29 | "id": "Plan2",
30 | "title": "Dispose Garbage",
31 | "label": "10 mins",
32 | "description": "Sort out recyclable and waste as needed"
33 | },
34 | {
35 | "id": "Plan3",
36 | "title": "Write Blog",
37 | "label": "30 mins",
38 | "description": "Can AI make memes?"
39 | },
40 | {
41 | "id": "Plan4",
42 | "title": "Pay Rent",
43 | "label": "5 mins",
44 | "description": "Transfer to bank account"
45 | }
46 | ]
47 | },
48 | {
49 | "id": "tomorrow",
50 | "title": "Tomorrow",
51 | "label": "0/0",
52 | "cards": []
53 | }
54 | ]
55 | }
--------------------------------------------------------------------------------
/stories/drag.css:
--------------------------------------------------------------------------------
1 | .draggingCard {
2 | background-color: #7fffd4;
3 | border: 1px dashed #a5916c;
4 | transform: rotate(2deg);
5 | }
6 |
7 | .draggingLane {
8 | background-color: #ffaecf;
9 | transform: rotate(2deg);
10 | border: 1px dashed #a5916c;
11 | }
12 |
--------------------------------------------------------------------------------
/stories/helpers/debug.js:
--------------------------------------------------------------------------------
1 | export default (message) => {
2 | if (process.env.NODE_ENV === 'test') { return }
3 | if (typeof message === 'object') {
4 | console.dir(message)
5 | } else {
6 | console.log(message)
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/stories/helpers/i18n.js:
--------------------------------------------------------------------------------
1 | import i18n from "i18next";
2 | import { initReactI18next } from "react-i18next";
3 |
4 | import resources from 'rt/locales'
5 |
6 | i18n
7 | .use(initReactI18next) // passes i18n down to react-i18next
8 | .init({
9 | resources,
10 | lng: "en"
11 | });
12 |
13 | export default i18n;
14 |
--------------------------------------------------------------------------------
/stories/index.js:
--------------------------------------------------------------------------------
1 | import './Base.story'
2 | import './DragDrop.story'
3 | import './Pagination.story'
4 | import './Interactions.story'
5 | import './Sort.story'
6 | import './Realtime.story'
7 | import './CollapsibleLanes.story'
8 | import './PaginationAndEvents.story'
9 | import './Tags.story'
10 | import './Styling.story'
11 | import './CustomCardWithDrag.story'
12 | import './CustomCard.story'
13 | import './CustomLaneHeader.story'
14 | import './CustomLaneFooter.story'
15 | import './CustomNewCardForm.story'
16 | import './CustomNewLaneSection.story'
17 | import './CustomNewLaneForm.story'
18 | import './CustomAddCardLink.story'
19 | import './AsyncLoad.story'
20 | import './RestrictedLanes.story'
21 | import './EditableBoard.story'
22 | import './MultipleBoards.story'
23 | import './I18n.story'
24 | import './Deprecations.story'
25 |
--------------------------------------------------------------------------------
/tests/Storyshots.test.js:
--------------------------------------------------------------------------------
1 | const initStoryshots = require('@storybook/addon-storyshots').default
2 | import 'jest-styled-components'
3 | initStoryshots({
4 | storyNameRegex: /^((?!.*?DontTest).)*$/
5 | })
6 |
--------------------------------------------------------------------------------
/tests/__mocks__/fileMock.js:
--------------------------------------------------------------------------------
1 | module.exports = 'test-file-stub'
2 |
--------------------------------------------------------------------------------