├── .babelrc ├── .eslintignore ├── .eslintrc ├── .github └── ISSUE_TEMPLATE │ ├── Bug_report.md │ └── Feature_request.md ├── .gitignore ├── .npmignore ├── .nvmrc ├── .prettierrc ├── .storybook ├── addons.js ├── config.js ├── manager-head.html └── preview-head.html ├── .travis.yml ├── CHANGELOG.md ├── CODEOWNERS ├── LICENSE.md ├── README.md ├── UPGRADE.md ├── bower.json ├── jest.config.js ├── package.json ├── react-trello.gif ├── src ├── actions │ ├── BoardActions.js │ └── LaneActions.js ├── components │ ├── AddCardLink.js │ ├── Card.js │ ├── Card │ │ └── Tag.js │ ├── Lane │ │ ├── LaneFooter.js │ │ ├── LaneHeader.js │ │ └── LaneHeader │ │ │ └── LaneMenu.js │ ├── Loader.js │ ├── NewCardForm.js │ ├── NewLaneForm.js │ ├── NewLaneSection.js │ └── index.js ├── controllers │ ├── Board.js │ ├── BoardContainer.js │ └── Lane.js ├── dnd │ ├── Container.js │ └── Draggable.js ├── helpers │ ├── LaneHelper.js │ ├── createTranslate.js │ └── deprecationWarnings.js ├── index.js ├── locales │ ├── en │ │ └── translation.json │ ├── index.js │ ├── pt-br │ │ └── translation.json │ └── ru │ │ └── translation.json ├── reducers │ └── BoardReducer.js ├── styles │ ├── Base.js │ ├── Elements.js │ └── Loader.js └── widgets │ ├── DeleteButton.js │ ├── EditableLabel.js │ ├── InlineInput.js │ ├── NewLaneTitleEditor.js │ └── index.js ├── stories ├── AsyncLoad.story.js ├── Base.story.js ├── CollapsibleLanes.story.js ├── CustomAddCardLink.story.js ├── CustomCard.story.js ├── CustomCardWithDrag.story.js ├── CustomLaneFooter.story.js ├── CustomLaneHeader.story.js ├── CustomNewCardForm.story.js ├── CustomNewLaneForm.story.js ├── CustomNewLaneSection.story.js ├── Deprecations.story.js ├── DragDrop.story.js ├── EditableBoard.story.js ├── I18n.story.js ├── Interactions.story.js ├── MultipleBoards.story.js ├── Pagination.story.js ├── PaginationAndEvents.story.js ├── Realtime.story.js ├── RestrictedLanes.story.js ├── Sort.story.js ├── Styling.story.js ├── Tags.story.js ├── board.css ├── data │ ├── base.json │ ├── board_with_custom_width.json │ ├── collapsible.json │ ├── data-sort.json │ ├── drag-drop.json │ └── other-board.json ├── drag.css ├── helpers │ ├── debug.js │ └── i18n.js └── index.js ├── tests ├── Storyshots.test.js ├── __mocks__ │ └── fileMock.js └── __snapshots__ │ └── Storyshots.test.js.snap └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "esmodules": true 8 | } 9 | } 10 | ], 11 | "@babel/preset-react" 12 | ], 13 | "plugins": [ 14 | "@babel/plugin-transform-template-literals", 15 | "@babel/plugin-proposal-class-properties", 16 | "@babel/plugin-transform-async-to-generator", 17 | "@babel/plugin-transform-runtime", 18 | ["module-resolver", { 19 | "alias": { "rt": "./src/" } 20 | }] 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/webpack.config.js 3 | /dist/ 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "rules": { 4 | "strict": 1, 5 | "no-undef": 1, 6 | "no-fallthrough": 1, 7 | "react/prop-types": 2 8 | }, 9 | "extends": ["standard", "standard-react", "plugin:prettier/recommended"], 10 | "env": { 11 | "browser": true, 12 | "node": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .DS_Store 4 | coverage 5 | .idea 6 | npm-debug.log 7 | dist/ 8 | .history/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | src 4 | test 5 | examples 6 | coverage 7 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v10.22.0 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "bracketSpacing": false, 6 | "jsxBracketSameLine": true, 7 | "arrowParens": "avoid", 8 | "semi": false, 9 | "tabWidth": 2, 10 | "useTabs": false 11 | } 12 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-options/register' 2 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import {addDecorator, configure} from '@storybook/react' 2 | import {withInfo} from '@storybook/addon-info' 3 | import {withOptions} from '@storybook/addon-options' 4 | 5 | addDecorator( 6 | withOptions({ 7 | name: 'react-trello', 8 | url: 'https://github.com/rcdexta/react-trello', 9 | goFullScreen: false, 10 | showStoriesPanel: true, 11 | showSearchBox: false, 12 | addonPanelInRight: false, 13 | showAddonPanel: false 14 | }) 15 | ) 16 | 17 | addDecorator( 18 | withInfo({ 19 | header: true, 20 | inline: false, 21 | source: true, 22 | propTables: false 23 | }) 24 | ) 25 | 26 | function loadStories() { 27 | require('../stories') 28 | } 29 | 30 | configure(loadStories, module) 31 | -------------------------------------------------------------------------------- /.storybook/manager-head.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcdexta/react-trello/29bfd1fe4e33da02e9b2a969d61dc2e61c5f5f84/.storybook/manager-head.html -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - ~/.npm 5 | notifications: 6 | email: true 7 | node_js: 8 | - '10.22.0' 9 | - '14.15.1' 10 | after_success: 11 | - npx semantic-release 12 | branches: 13 | except: 14 | - /^v\d+\.\d+\.\d+$/ 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [2.2.4] - 2020-03-08 9 | 10 | ### Fixes 11 | 12 | * [#354] Getting errors on each drag and drop using react-trello/smooth-dnd 13 | * [#317] Upgrade smooth-dnd to the latest version 14 | 15 | 16 | ## [2.2.3] - 2019-08-22 17 | 18 | ### Fixes 19 | 20 | * [#312] Lane metadata not being passed to CustomLaneHeader 21 | 22 | ## [2.2.2] - 2019-07-30 23 | 24 | ### Improvements 25 | 26 | * LaneHeaderComponent specified two editLaneTitle props, causing a warning to 27 | be shown 28 | * Remove duplicated editLaneTitle propType 29 | 30 | ### Fixes 31 | 32 | * [#306] Delete Icon not there in Custom Card Component 33 | 34 | ## [2.2.1] - 2019-07-22 35 | 36 | ### Improvements 37 | 38 | * Cursor on lane header respects draggable 39 | * Improve new lane title editor (auto close on click outside) 40 | 41 | ### Fixes 42 | 43 | * [#298] onCardDelete Event doesn't get fired anymore 44 | 45 | ## [2.2.0] - 2019-07-20 46 | 47 | ### Added 48 | 49 | * Availability to hide Add Card button for specific lane 50 | * Internationalization support (property `t` and `lang`) 51 | * Russian translation 52 | * Dependency Injection mechanisim for all components customization 53 | (property `components`) 54 | * Add `onCardMoveAcrossLanes` handler property, called when a card is moved across lanes 55 | * Add `onDeleteLane` handler property 56 | * Add `laneStyle` property 57 | * Add `editLaneTitle` and `onLaneUpdate` props (availability to inline edit lane 58 | title) 59 | 60 | ### Improvements 61 | 62 | * Removed CSS style hardcoding (except `react-popover`) 63 | * Upgrade dev dependencies and remove unnecessary pkgs 64 | * Suppress debug prints in tests 65 | * Add babel-plugin-module-resolver to root as ./src 66 | * Removed `react-popover`'s CSS classes. 67 | 68 | 69 | ### Fixes 70 | 71 | * [#203] fixes Cannot drop a card near the bottom of a lane 72 | * [#205] fix argument names in handleLaneDragEnd 73 | * [#201] Fixed Warning: Failed prop type by replacing `react-popover` with `react-popopo` 74 | 75 | ### Breaking changes 76 | 77 | * Removed props `addLaneTitle` and `addCardTitle`. Use `t('Add another lane')` and `t('Click to add card')` instead of it. 78 | * Removed props `customLaneHeader`, `newCardTemplate`, `newLaneTemplate`, `customCardLayout`. Use `components` property instead of it. 79 | 80 | Refer [upgrade instructions](UPGRADE.md) to migrate to new version. 81 | 82 | ## [0.0.0] - [2.1.4] 83 | 84 | Lookin into `git log` 85 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | # These owners will be the default owners for everything in the repo. 5 | * @rcdexta 6 | * @dapi 7 | 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 RC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Trello 2 | 3 | Pluggable components to add a Trello (like) kanban board to your application 4 | 5 | [![Build Status](https://travis-ci.org/rcdexta/react-trello.svg?branch=master)](https://travis-ci.org/rcdexta/react-trello) 6 | [![yarn version](https://badge.fury.io/js/react-trello.svg)](https://badge.fury.io/js/react-trello) 7 | [![bundlephobia.com](https://img.shields.io/bundlephobia/minzip/react-trello.svg)](https://bundlephobia.com/result?p=react-trello) 8 | 9 | > This library is not affiliated, associated, authorized, endorsed by or in any way officially connected to Trello, Inc. `Trello` is a registered trademark of Atlassian, Inc. 10 | 11 | #### Basic Demo 12 | [![Edit react-trello-example](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/1o3zj95m9j) 13 | 14 | #### Features Showcase 15 | [Storybook](https://rcdexta.github.io/react-trello/) 16 | 17 | ## Features 18 | 19 | ![alt tag](https://raw.githubusercontent.com/rcdexta/react-trello/master/react-trello.gif) 20 | 21 | * Responsive and extensible 22 | * Easily pluggable into existing react application 23 | * Supports pagination when scrolling individual lanes 24 | * Drag-And-Drop on cards and lanes (compatible with touch devices) 25 | * Edit functionality to add/delete cards 26 | * Custom elements to define lane and card appearance 27 | * Event bus for triggering events externally (e.g.: adding or removing cards based on events coming from backend) 28 | * Inline edit lane's title 29 | 30 | ## Getting Started 31 | 32 | Install using npm or yarn 33 | 34 | ```bash 35 | $ npm install --save react-trello 36 | ``` 37 | 38 | or 39 | 40 | ```bash 41 | $ yarn add react-trello 42 | ``` 43 | 44 | 45 | ## Usage 46 | 47 | The `Board` component takes a prop called `data` that contains all the details related to rendering the board. A sample data json is given here to illustrate the contract: 48 | 49 | ```javascript 50 | const data = { 51 | lanes: [ 52 | { 53 | id: 'lane1', 54 | title: 'Planned Tasks', 55 | label: '2/2', 56 | cards: [ 57 | {id: 'Card1', title: 'Write Blog', description: 'Can AI make memes', label: '30 mins', draggable: false}, 58 | {id: 'Card2', title: 'Pay Rent', description: 'Transfer via NEFT', label: '5 mins', metadata: {sha: 'be312a1'}} 59 | ] 60 | }, 61 | { 62 | id: 'lane2', 63 | title: 'Completed', 64 | label: '0/0', 65 | cards: [] 66 | } 67 | ] 68 | } 69 | ``` 70 | 71 | `draggable` property of Card object is `true` by default. 72 | 73 | The data is passed to the board component and that's it. 74 | 75 | ```jsx 76 | import React from 'react' 77 | import Board from 'react-trello' 78 | 79 | export default class App extends React.Component { 80 | render() { 81 | return 82 | } 83 | } 84 | ``` 85 | 86 | Refer to storybook for detailed examples: https://rcdexta.github.io/react-trello/ 87 | 88 | Also refer to the sample project that uses react-trello as illustration: https://github.com/rcdexta/react-trello-example 89 | 90 | ## Use edge version of project (current master branch) 91 | 92 | ```bash 93 | $ yarn add rcdexta/react-trello 94 | ``` 95 | 96 | and 97 | 98 | ```javascript 99 | import Board from 'react-trello/src' 100 | ``` 101 | 102 | ## Upgrade 103 | 104 | Breaking changes. Since version 2.2 these properties are removed: `addLaneTitle`, `addCardLink`, `customLaneHeader`, `newCardTemplate`, `newLaneTemplate`, 105 | and `customCardLayout` with `children` element. 106 | 107 | Follow [upgrade instructions](UPGRADE.md) to make easy migration. 108 | 109 | ## Properties 110 | 111 | This is the container component that encapsulates the lanes and cards 112 | 113 | ### Required parameters 114 | 115 | | Name | Type | Description | 116 | | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ | 117 | | data | object | Actual board data in the form of json | 118 | 119 | ### Optionable flags 120 | 121 | | Name | Type | Description | 122 | | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ | 123 | | draggable | boolean | Makes all cards and lanes draggable. Default: false | 124 | | laneDraggable | boolean | Set to false to disable lane dragging. Default: true | 125 | | cardDraggable | boolean | Set to false to disable card dragging. Default: true | 126 | | collapsibleLanes | boolean | Make the lanes with cards collapsible. Default: false | 127 | | editable | boolean | Makes the entire board editable. Allow cards to be added or deleted Default: false | 128 | | canAddLanes | boolean | Allows new lanes to be added to the board. Default: false | 129 | | hideCardDeleteIcon | boolean | Disable showing the delete icon to the top right corner of the card (when board is editable) | 130 | | editLaneTitle | boolean | Allow inline lane title edit Default: false | 131 | 132 | 133 | ### Callbacks and handlers 134 | 135 | | Name | Type | Description | 136 | | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ | 137 | | handleDragStart | function | Callback function triggered when card drag is started: `handleDragStart(cardId, laneId)` | 138 | | handleDragEnd | function | Callback function triggered when card drag ends, return false if you want to cancel drop: `handleDragEnd(cardId, sourceLaneId, targetLaneId, position, cardDetails)` | 139 | | handleLaneDragStart | function | Callback function triggered when lane drag is started: `handleLaneDragStart(laneId)` | 140 | | handleLaneDragEnd | function | Callback function triggered when lane drag ends: `handleLaneDragEnd(removedIndex, addedIndex, payload)` | 141 | | onDataChange | function | Called everytime the data changes due to user interaction or event bus: `onDataChange(newData)` | 142 | | onCardClick | function | Called when a card is clicked: `onCardClick(cardId, metadata, laneId)` | 143 | | onCardAdd | function | Called when a new card is added: `onCardAdd(card, laneId)` | 144 | | onBeforeCardDelete | function | Called before delete a card, please call the `callback()` if confirm to delete a card: `onConfirmCardDelete(callback)` 145 | | onCardDelete | function | Called when a card is deleted: `onCardDelete(cardId, laneId)` | 146 | | onCardMoveAcrossLanes | function | Called when a card is moved across lanes `onCardMoveAcrossLanes(fromLaneId, toLaneId, cardId, index)` | 147 | | onLaneAdd | function | Called when a new lane is added: `onLaneAdd(params)` | 148 | | onLaneDelete | function | Called when a lane is deleted `onLaneDelete(laneId)` | 149 | | onLaneUpdate | function | Called when a lane attributes are updated `onLaneUpdate(laneId, data)` | 150 | | onLaneClick | function | Called when a lane is clicked `onLaneClick(laneId)`. Card clicks are not propagated to lane click event | 151 | | onLaneScroll | function | Called when a lane is scrolled to the end: `onLaneScroll(requestedPage, laneId)` | 152 | 153 | ### Other functions 154 | 155 | | Name | Type | Description | 156 | | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ | 157 | | eventBusHandle | function | This is a special function that providers a publishHook to pass new events to the board. See details in Publish Events section | 158 | | laneSortFunction | function | Used to specify the logic to sort cards on a lane: `laneSortFunction(card1, card2)` | 159 | 160 | ### I18n support 161 | 162 | | Name | Type | Description | 163 | | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ | 164 | | lang | string | Language of compiled texts ("en", "ru", "pt-br"). Default is "en" | 165 | | t | function | Translation function. You can specify either one key as a `String`. Look into ./src/locales for keys list | 166 | 167 | ### Style customization 168 | 169 | | Name | Type | Description | 170 | | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ | 171 | | style | object | Pass CSS style props to board container | 172 | | cardStyle | object | CSS style for every cards | 173 | | laneStyle | object | CSS style for every lanes | 174 | | tagStyle | object | If cards have tags, use this prop to modify their style | 175 | | cardDragClass | string | CSS class to be applied to Card when being dragged | 176 | | cardDropClass | string | CSS class to be applied to Card when being dropped | 177 | | laneDragClass | string | CSS class to be applied to Lane when being dragged | 178 | | laneDropClass | string | CSS class to be applied to Lane when being dropped | 179 | | components | object | Map of customised components. [List](src/components/index.js) of available. | 180 | 181 | 182 | ### Lane specific props 183 | 184 | | Name | Type | Description | 185 | | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ | 186 | | id | string | ID of lane | 187 | | style | object | Pass CSS style props to lane container | 188 | | labelStyle | object | Pass CSS style props of label | 189 | | cardStyle | object | Pass CSS style props for cards in this lane | 190 | | disallowAddingCard | boolean | Disallow adding card button in this lane (default: false) | 191 | 192 | 193 | Refer to `stories` folder for examples on many more options for customization. 194 | 195 | ## Editable Board 196 | 197 | It is possible to make the entire board editable by setting the `editable` prop to true. This switch prop will enable existing cards to be deleted and show a `Add Card` link at the bottom of each lane, clicking which will show an inline editable new card. 198 | 199 | Check out the [editable board story](https://rcdexta.github.io/react-trello/?selectedKind=Editable%20Board&selectedStory=Add%2FDelete%20Cards&full=0&down=0&left=1&panelRight=0) and its corresponding [source code](https://github.com/rcdexta/react-trello/blob/master/stories/EditableBoard.story.js) for more details. 200 | 201 | ## Styling and customization 202 | 203 | There are three ways to apply styles to the library components including `Board`, `Lane` or `Card`: 204 | 205 | ### 1. Predefined CSS classnames 206 | 207 | Use the predefined css classnames attached to these elements that go by `.react-trello-lane`, `.react-trello-card`, `.react-trello-board`: 208 | 209 | ```css 210 | .react-trello-lane { 211 | border: 0; 212 | background-color: initial; 213 | } 214 | ``` 215 | 216 | ### 2. Pass custom style attributes as part of data. 217 | 218 | This method depends on used `Card` and `Lane` components. 219 | 220 | ```javascript 221 | const data = { 222 | lanes: [ 223 | { 224 | id: 'lane1', 225 | title: 'Planned Tasks', 226 | style: { backgroundColor: 'yellow' }, // Style of Lane 227 | cardStyle: { backgroundColor: 'blue' } // Style of Card 228 | ... 229 | }; 230 | 231 | 235 | ``` 236 | 237 | Storybook example - [stories/Styling.story.js](stories/Styling.story.js) 238 | 239 | ### 3. Completely customize the look-and-feel by using components dependency injection. 240 | 241 | You can override any of used components (ether one or completery all) 242 | 243 | ```javascript 244 | const components = { 245 | GlobalStyle: MyGlobalStyle, // global style created with method `createGlobalStyle` of `styled-components` 246 | LaneHeader: MyLaneHeader, 247 | Card: MyCard, 248 | AddCardLink: MyAddCardLink, 249 | ... 250 | }; 251 | 252 | 253 | ``` 254 | 255 | Total list of customizable components: [src/components/index.js ](src/components/index.js) 256 | 257 | Refer to [components definitions](src/components) to discover their properties list and types. 258 | 259 | Refer more examples in storybook. 260 | 261 | ## Publish Events 262 | 263 | When defining the board, it is possible to obtain a event hook to the component to publish new events later after the board has been rendered. Refer the example below: 264 | 265 | ```javascript 266 | let eventBus = undefined 267 | 268 | const setEventBus = (handle) => { 269 | eventBus = handle 270 | } 271 | //To add a card 272 | eventBus.publish({type: 'ADD_CARD', laneId: 'COMPLETED', card: {id: "M1", title: "Buy Milk", label: "15 mins", description: "Also set reminder"}}) 273 | 274 | //To update a card 275 | eventBus.publish({type: 'UPDATE_CARD', laneId: 'COMPLETED', card: {id: "M1", title: "Buy Milk (Updated)", label: "20 mins", description: "Also set reminder (Updated)"}}) 276 | 277 | //To remove a card 278 | eventBus.publish({type: 'REMOVE_CARD', laneId: 'PLANNED', cardId: "M1"}) 279 | 280 | //To move a card from one lane to another. index specifies the position to move the card to in the target lane 281 | eventBus.publish({type: 'MOVE_CARD', fromLaneId: 'PLANNED', toLaneId: 'WIP', cardId: 'Plan3', index: 0}) 282 | 283 | //To update the lanes 284 | eventBus.publish({type: 'UPDATE_LANES', lanes: newLaneData}) 285 | 286 | 287 | ``` 288 | 289 | The first event in the above example will move the card `Buy Milk` from the planned lane to completed lane. We expect that this library can be wired to a backend push api that can alter the state of the board in realtime. 290 | 291 | ## I18n and text translations 292 | 293 | ### Custom text translation function 294 | 295 | Pass translation function to provide custom or localized texts: 296 | 297 | ```javascript 298 | 299 | // If your translation table is flat 300 | // 301 | // For example: { 'placeholder.title': 'some text' } 302 | const customTranslation = (key) => TRANSLATION_TABLE[key] 303 | 304 | // If your translation table has nested hashes (provided translations table is it) 305 | // 306 | // For example: { 'placeholder': { 'title': 'some text' } } 307 | import { createTranslate } from 'react-trello' 308 | const customTranslation = createTranslate(TRANSLATION_TABLE) 309 | 310 | 311 | ``` 312 | 313 | List of available keys - [locales/en/translation.json](https://github.com/rcdexta/react-trello/blob/master/locales/en/translation.json) 314 | 315 | 316 | ### react-i18next example 317 | 318 | ```javascript 319 | import { withTranslation } from 'react-i18next'; 320 | 321 | const I18nBoard = withTranslation()(Board) 322 | ``` 323 | 324 | ## Compatible Browsers 325 | 326 | Tested to work with following browsers using [Browserling](https://www.browserling.com/): 327 | 328 | * Chrome 60 or above 329 | * Firefox 52 or above 330 | * Opera 51 or above 331 | * Safari 4.0 or above 332 | * Microsoft Edge 15 or above 333 | 334 | ## Logging 335 | 336 | Pass environment variable `REDUX_LOGGING` as true to enable Redux logging in any environment 337 | 338 | ## Development 339 | 340 | ``` 341 | cd react-trello/ 342 | yarn install 343 | yarn run storybook 344 | ``` 345 | 346 | ### Scripts 347 | 348 | 1. `yarn run lint` : Lint all js files 349 | 2. `yarn run lintfix` : fix linting errors of all js files 350 | 3. `yarn run semantic-release` : make a release. Leave it for CI to do. 351 | 4. `yarn run storybook`: Start developing by using storybook 352 | 5. `yarn run test` : Run tests. tests file should be written as `*.test.js` and using ES2015 353 | 6. `yarn run test:watch` : Watch tests while writing 354 | 7. `yarn run test:cover` : Show coverage report of your tests 355 | 8. `yarn run test:report` : Report test coverage to codecov.io. Leave this for CI 356 | 9. `yarn run build`: transpile all ES6 component files into ES5(commonjs) and put it in `dist` directory 357 | 10. `yarn run docs`: create static build of storybook in `docs` directory that can be used for github pages 358 | 359 | Learn how to write stories [here](https://storybook.js.org/basics/writing-stories/) 360 | 361 | ### Maintainers 362 | 363 | 364 | 365 | 369 | 370 | 374 | 375 | 376 |
366 | 367 |
rcdexta
368 |
371 | 372 |
dapi
373 |
377 | 378 | 379 | ### License 380 | 381 | MIT 382 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | # Upgrade instictions 2 | 3 | ## Upgrade from 2.1 to 2.2 4 | 5 | ### Texts 6 | 7 | Boths text properties are removed: `addLaneTitle` and `addCardLink`. 8 | Use translation function `t` to full control texts: 9 | 10 | | Legacy property | Key name in components object| 11 | | ------------------- | ---------------------------- | 12 | | addLaneTitle | "Add another lane" | 13 | | addCardLink | "Click to add card" | 14 | 15 | [Complete list of available translation keys](src/locales/en/translation.json) 16 | 17 | #### Migration example 18 | 19 | Instead of 20 | 21 | ```javascript 22 | 26 | ``` 27 | 28 | Use 29 | 30 | ```javascript 31 | 32 | import { createTranslate } from 'react-trello' 33 | 34 | const TEXTS = { 35 | "Add another lane": "NEW LANE", 36 | "Click to add card": "Click to add card", 37 | "Delete lane": "Delete lane", 38 | "Lane actions": "Lane actions", 39 | "button": { 40 | "Add lane": "Add lane", 41 | "Add card": "ADD CARD", 42 | "Cancel": "Cancel" 43 | }, 44 | "placeholder": { 45 | "title": "title", 46 | "description": "description", 47 | "label": "label" 48 | } 49 | } 50 | 51 | ``` 52 | 53 | ### Components customization 54 | 55 | These properties are removed: `addCardLink`, `customLaneHeader`, `newCardTemplate`, `newLaneTemplate` 56 | and `customCardLayout` with `children` element. 57 | 58 | You must use `components` property, that contains map of custom 59 | `React.Component`'s (not elements/templates) 60 | 61 | | Legacy property | Key name in components map| 62 | | ------------------- | ---------------------------- | 63 | | addCardLink | AddCardLink | 64 | | customLaneHeader | LaneHeader | 65 | | newCardTemplate | NewCardForm | 66 | | newLaneTemplate | NewLaneSection | 67 | | customCardLayout (children) | Card | 68 | 69 | Full list of available components keys - 70 | [src/components/index.js](src/components/index.js) 71 | 72 | #### Migration example 73 | 74 | Instead of 75 | 76 | ```javascript 77 | New Card} 79 | customLaneHeader={} 80 | newCardTemplate ={} 81 | newLaneTemplate ={} 82 | customCardLayout 83 | > 84 | 85 | 86 | 87 | ``` 88 | 89 | Use 90 | 91 | ```javascript 92 | const components = { 93 | AddCardLink: () => , 94 | LaneHeader: CustomLaneHeader, 95 | NewCardForm: NewCard, 96 | NewLaneSection: NewLane, 97 | Card: CustomCard 98 | }; 99 | 100 | ``` 101 | 102 | That's all. Have a nice day! ) 103 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-trello", 3 | "description": "Pluggable components to add a trello like kanban board to your application (browserified)", 4 | "main": "dist/index.js", 5 | "authors": [ 6 | "RC", 7 | "Prakash" 8 | ], 9 | "license": "MIT", 10 | "keywords": [ 11 | "react", 12 | "trello", 13 | "board" 14 | ], 15 | "homepage": "https://github.com/rcdexta/react-trello", 16 | "ignore": [ 17 | "**/.*", 18 | "node_modules", 19 | "bower_components", 20 | "test", 21 | "tests" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | transform: { 6 | '^.+\\.jsx?$': 'babel-jest' 7 | }, 8 | 'moduleNameMapper': { 9 | '\\.(jpg|png|gif|svg)$': '/tests/__mocks__/fileMock.js', 10 | "\\.(css)$": "identity-obj-proxy" 11 | }, 12 | 'coveragePathIgnorePatterns': [ 13 | '/stories/', 14 | '/.storybook/', 15 | '/node_modules/', 16 | 'story(.*).tsx' 17 | ], 18 | collectCoverage: true 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-trello", 3 | "version": "2.4.1", 4 | "description": "Pluggable components to add a trello like kanban board to your application", 5 | "main": "dist/index.js", 6 | "files": [ 7 | "dist", 8 | "README" 9 | ], 10 | "scripts": { 11 | "prepublish": "npm run build", 12 | "storybook": "start-storybook -p 9002", 13 | "test": "jest", 14 | "test:watch": "jest --watch", 15 | "test:cover": "istanbul cover -x *.test.js _mocha -- -R spec --require tests/config/setup 'tests/**/*.test.js'", 16 | "test:report": "cat ./coverage/lcov.info | codecov && rm -rf ./coverage", 17 | "build": "babel src --out-dir dist --copy-files", 18 | "docs": "build-storybook -o docs", 19 | "commit": "git cz", 20 | "deploy-storybook": "storybook-to-ghpages", 21 | "format": "pretty-quick \"src/**/*.js\"", 22 | "size": "size-limit" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/rcdexta/react-trello" 27 | }, 28 | "keywords": [ 29 | "react", 30 | "trello", 31 | "board" 32 | ], 33 | "author": "RC, Prakash", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/rcdexta/react-trello/issues" 37 | }, 38 | "homepage": "https://github.com/rcdexta/react-trello", 39 | "dependencies": { 40 | "autosize": "^4.0.2", 41 | "classnames": "^2.2.6", 42 | "immutability-helper": "^2.8.1", 43 | "lodash": "^4.17.11", 44 | "prop-types": "^15.7.2", 45 | "react-popopo": "^2.1.9", 46 | "react-redux": "^5.0.7", 47 | "redux": "^4.0.0", 48 | "redux-actions": "^2.6.1", 49 | "redux-logger": "^3.0.6", 50 | "trello-smooth-dnd": "1.0.0", 51 | "uuid": "^3.3.2" 52 | }, 53 | "devDependencies": { 54 | "@babel/cli": "7.1.2", 55 | "@babel/core": "^7.1.2", 56 | "@babel/plugin-proposal-class-properties": "^7.1.0", 57 | "@babel/plugin-transform-async-to-generator": "7.4.4", 58 | "@babel/plugin-transform-runtime": "7.1.0", 59 | "@babel/plugin-transform-template-literals": "^7.4.4", 60 | "@babel/preset-env": "7.1.0", 61 | "@babel/preset-react": " 7.0.0", 62 | "@storybook/addon-info": "5.3.18", 63 | "@storybook/addon-options": "5.3.18", 64 | "@storybook/addon-storyshots": "5.3.18", 65 | "@storybook/cli": "5.3.18", 66 | "@storybook/react": "5.3.18", 67 | "@storybook/storybook-deployer": "^2.8.7", 68 | "autoprefixer": "^9.6.0", 69 | "babel-core": "^7.0.0-0", 70 | "babel-eslint": "^10.0.1", 71 | "babel-jest": "^23.6.0", 72 | "babel-loader": "^8.0.6", 73 | "babel-plugin-module-resolver": "^3.2.0", 74 | "babel-preset-react": "^6.24.1", 75 | "codecov.io": "^0.1.6", 76 | "commitizen": "^3.1.1", 77 | "cz-conventional-changelog": "^2.1.0", 78 | "eslint": "^5.16.0", 79 | "eslint-config-prettier": "^3.1.0", 80 | "eslint-config-standard": "^12.0.0", 81 | "eslint-config-standard-react": "^7.0.2", 82 | "eslint-plugin-import": "^2.14.0", 83 | "eslint-plugin-node": "^7.0.1", 84 | "eslint-plugin-prettier": "^3.1.0", 85 | "eslint-plugin-promise": "^4.1.1", 86 | "eslint-plugin-react": "^7.11.1", 87 | "eslint-plugin-standard": "^4.0.0", 88 | "eventsource-polyfill": "^0.9.6", 89 | "husky": "^1.1.2", 90 | "i18next": "^17.0.3", 91 | "identity-obj-proxy": "^3.0.0", 92 | "jest": "^24.8.0", 93 | "jest-cli": "^24.8.0", 94 | "jest-styled-components": "^6.3.3", 95 | "prettier": "1.14.3", 96 | "pretty-quick": "^1.7.0", 97 | "react": "^16.8.0", 98 | "react-dom": "^16.8.0", 99 | "react-i18next": "^10.11.0", 100 | "react-test-renderer": "^16.8.6", 101 | "semantic-release": "^17.2.3", 102 | "size-limit": "^1.3.7", 103 | "styled-components": "4.0.3" 104 | }, 105 | "peerDependencies": { 106 | "lodash": ">= 4.17.11", 107 | "react": "*", 108 | "react-dom": "*", 109 | "react-redux": ">= 5.0.7", 110 | "redux": ">= 4.0.0", 111 | "redux-actions": ">= 2.6.1", 112 | "redux-logger": ">= 3.0.6", 113 | "styled-components": ">= 4.0.3" 114 | }, 115 | "config": { 116 | "commitizen": { 117 | "path": "node_modules/cz-conventional-changelog" 118 | } 119 | }, 120 | "size-limit": [ 121 | { 122 | "path": "dist/**/*.js", 123 | "limit": "800 ms" 124 | } 125 | ] 126 | } 127 | -------------------------------------------------------------------------------- /react-trello.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcdexta/react-trello/29bfd1fe4e33da02e9b2a969d61dc2e61c5f5f84/react-trello.gif -------------------------------------------------------------------------------- /src/actions/BoardActions.js: -------------------------------------------------------------------------------- 1 | import {createAction} from 'redux-actions' 2 | 3 | export const loadBoard = createAction('LOAD_BOARD') 4 | export const addLane = createAction('ADD_LANE') 5 | -------------------------------------------------------------------------------- /src/actions/LaneActions.js: -------------------------------------------------------------------------------- 1 | import {createAction} from 'redux-actions' 2 | 3 | export const addCard = createAction('ADD_CARD') 4 | export const updateCard = createAction('UPDATE_CARD') 5 | export const removeCard = createAction('REMOVE_CARD') 6 | export const moveCardAcrossLanes = createAction('MOVE_CARD') 7 | export const updateCards = createAction('UPDATE_CARDS') 8 | export const updateLanes = createAction('UPDATE_LANES') 9 | export const updateLane = createAction('UPDATE_LANE') 10 | export const paginateLane = createAction('PAGINATE_LANE') 11 | export const moveLane = createAction('MOVE_LANE') 12 | export const removeLane = createAction('REMOVE_LANE') 13 | -------------------------------------------------------------------------------- /src/components/AddCardLink.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {AddCardLink} from 'rt/styles/Base' 3 | 4 | export default ({onClick, t, laneId}) => {t('Click to add card')} 5 | -------------------------------------------------------------------------------- /src/components/Card.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { 5 | MovableCardWrapper, 6 | CardHeader, 7 | CardRightContent, 8 | CardTitle, 9 | Detail, 10 | Footer 11 | } from 'rt/styles/Base' 12 | import InlineInput from 'rt/widgets/InlineInput' 13 | import Tag from './Card/Tag' 14 | import DeleteButton from 'rt/widgets/DeleteButton' 15 | 16 | class Card extends Component { 17 | onDelete = e => { 18 | this.props.onDelete() 19 | e.stopPropagation() 20 | } 21 | 22 | render() { 23 | const { 24 | showDeleteButton, 25 | style, 26 | tagStyle, 27 | onClick, 28 | onDelete, 29 | onChange, 30 | className, 31 | id, 32 | title, 33 | label, 34 | description, 35 | tags, 36 | cardDraggable, 37 | editable, 38 | t 39 | } = this.props 40 | 41 | const updateCard = (card) => { 42 | onChange({...card, id}) 43 | } 44 | 45 | return ( 46 | 52 | 53 | 54 | {editable ? updateCard({title: value})} /> : title} 55 | 56 | 57 | {editable ? updateCard({label: value})} /> : label} 58 | 59 | {showDeleteButton && } 60 | 61 | 62 | {editable ? updateCard({description: value})} /> : description} 63 | 64 | {tags && tags.length> 0 && ( 65 |
66 | {tags.map(tag => ( 67 | 68 | ))} 69 |
70 | )} 71 |
72 | ) 73 | } 74 | } 75 | 76 | Card.propTypes = { 77 | showDeleteButton: PropTypes.bool, 78 | onDelete: PropTypes.func, 79 | onClick: PropTypes.func, 80 | style: PropTypes.object, 81 | tagStyle: PropTypes.object, 82 | className: PropTypes.string, 83 | id: PropTypes.string.isRequired, 84 | title: PropTypes.string.isRequired, 85 | label: PropTypes.string, 86 | description: PropTypes.string, 87 | tags: PropTypes.array, 88 | } 89 | 90 | Card.defaultProps = { 91 | showDeleteButton: true, 92 | onDelete: () => {}, 93 | onClick: () => {}, 94 | style: {}, 95 | tagStyle: {}, 96 | title: 'no title', 97 | description: '', 98 | label: '', 99 | tags: [], 100 | className: '' 101 | } 102 | 103 | export default Card 104 | -------------------------------------------------------------------------------- /src/components/Card/Tag.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import PropTypes from 'prop-types' 3 | import {TagSpan} from 'rt/styles/Base' 4 | 5 | class Tag extends Component { 6 | render() { 7 | const {title, color, bgcolor, tagStyle, ...otherProps} = this.props 8 | const style = {color: color || 'white', backgroundColor: bgcolor || 'orange', ...tagStyle} 9 | return ( 10 | 11 | {title} 12 | 13 | ) 14 | } 15 | } 16 | 17 | Tag.propTypes = { 18 | title: PropTypes.string.isRequired, 19 | color: PropTypes.string, 20 | bgcolor: PropTypes.string, 21 | tagStyle: PropTypes.object 22 | } 23 | 24 | export default Tag 25 | -------------------------------------------------------------------------------- /src/components/Lane/LaneFooter.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import {LaneFooter} from 'rt/styles/Base' 4 | 5 | import { 6 | CollapseBtn, 7 | ExpandBtn, 8 | } from 'rt/styles/Elements' 9 | 10 | export default ({onClick, collapsed}) => {collapsed ? : } 11 | -------------------------------------------------------------------------------- /src/components/Lane/LaneHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import InlineInput from 'rt/widgets/InlineInput' 4 | import {Title, LaneHeader, RightContent } from 'rt/styles/Base' 5 | import LaneMenu from './LaneHeader/LaneMenu' 6 | 7 | const LaneHeaderComponent = ({ 8 | updateTitle, canAddLanes, onDelete, onDoubleClick, editLaneTitle, label, title, titleStyle, labelStyle, t, laneDraggable 9 | }) => { 10 | 11 | return ( 12 | 13 | 14 | {editLaneTitle ? 15 | <InlineInput value={title} border placeholder={t('placeholder.title')} resize='vertical' onSave={updateTitle} /> : 16 | title 17 | } 18 | 19 | {label && ( 20 | 21 | {label} 22 | 23 | )} 24 | {canAddLanes && } 25 | 26 | ) 27 | } 28 | 29 | LaneHeaderComponent.propTypes = { 30 | updateTitle: PropTypes.func, 31 | editLaneTitle: PropTypes.bool, 32 | canAddLanes: PropTypes.bool, 33 | laneDraggable: PropTypes.bool, 34 | label: PropTypes.string, 35 | title: PropTypes.string, 36 | onDelete: PropTypes.func, 37 | onDoubleClick: PropTypes.func, 38 | t: PropTypes.func.isRequired 39 | } 40 | 41 | LaneHeaderComponent.defaultProps = { 42 | updateTitle: () => {}, 43 | editLaneTitle: false, 44 | canAddLanes: false 45 | } 46 | 47 | export default LaneHeaderComponent; 48 | -------------------------------------------------------------------------------- /src/components/Lane/LaneHeader/LaneMenu.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { Popover } from 'react-popopo' 5 | 6 | import { CustomPopoverContent, CustomPopoverContainer } from 'rt/styles/Base' 7 | 8 | import { 9 | LaneMenuTitle, 10 | LaneMenuHeader, 11 | LaneMenuContent, 12 | DeleteWrapper, 13 | LaneMenuItem, 14 | GenDelButton, 15 | MenuButton, 16 | } from 'rt/styles/Elements' 17 | 18 | const TEST= PropTypes.elementType; 19 | 20 | const LaneMenu = ({t, onDelete}) => ( 21 | ⋮}> 22 | 23 | {t('Lane actions')} 24 | 25 | 26 | 27 | 28 | 29 | {t('Delete lane')} 30 | 31 | 32 | ) 33 | 34 | export default LaneMenu; 35 | -------------------------------------------------------------------------------- /src/components/Loader.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {LoaderDiv, LoadingBar} from 'rt/styles/Loader' 3 | 4 | const Loader = () => ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | ) 12 | 13 | export default Loader 14 | -------------------------------------------------------------------------------- /src/components/NewCardForm.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import PropTypes from 'prop-types' 3 | import { 4 | CardForm, 5 | CardHeader, 6 | CardRightContent, 7 | CardTitle, 8 | CardWrapper, 9 | Detail 10 | } from 'rt/styles/Base' 11 | import {AddButton, CancelButton} from 'rt/styles/Elements' 12 | import EditableLabel from 'rt/widgets/EditableLabel' 13 | 14 | class NewCardForm extends Component { 15 | updateField = (field, value) => { 16 | this.setState({[field]: value}) 17 | } 18 | 19 | handleAdd = () => { 20 | this.props.onAdd(this.state) 21 | } 22 | 23 | render() { 24 | const {onCancel, t} = this.props 25 | return ( 26 | 27 | 28 | 29 | 30 | this.updateField('title', val)} autoFocus /> 31 | 32 | 33 | this.updateField('label', val)} /> 34 | 35 | 36 | 37 | this.updateField('description', val)} /> 38 | 39 | 40 | {t('button.Add card')} 41 | {t('button.Cancel')} 42 | 43 | ) 44 | } 45 | } 46 | 47 | NewCardForm.propTypes = { 48 | onCancel: PropTypes.func.isRequired, 49 | onAdd: PropTypes.func.isRequired, 50 | t: PropTypes.func.isRequired, 51 | } 52 | 53 | NewCardForm.defaultProps = { 54 | } 55 | 56 | export default NewCardForm 57 | -------------------------------------------------------------------------------- /src/components/NewLaneForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { LaneTitle, NewLaneButtons, Section } from 'rt/styles/Base' 4 | import { AddButton, CancelButton } from 'rt/styles/Elements' 5 | import NewLaneTitleEditor from 'rt/widgets/NewLaneTitleEditor' 6 | import uuidv1 from 'uuid/v1' 7 | 8 | class NewLane extends Component { 9 | handleSubmit = () => { 10 | this.props.onAdd({ 11 | id: uuidv1(), 12 | title: this.getValue() 13 | }) 14 | } 15 | 16 | getValue = () => this.refInput.getValue() 17 | 18 | onClickOutside = (a,b,c) => { 19 | if (this.getValue().length > 0) { 20 | this.handleSubmit() 21 | } else { 22 | this.props.onCancel() 23 | } 24 | } 25 | 26 | render() { 27 | const { onCancel, t } = this.props 28 | return ( 29 |
30 | 31 | (this.refInput = ref)} 33 | placeholder={t('placeholder.title')} 34 | onCancel={this.props.onCancel} 35 | onSave={this.handleSubmit} 36 | resize='vertical' 37 | border 38 | autoFocus 39 | /> 40 | 41 | 42 | {t('button.Add lane')} 43 | {t('button.Cancel')} 44 | 45 |
46 | ) 47 | } 48 | } 49 | 50 | NewLane.propTypes = { 51 | onCancel: PropTypes.func.isRequired, 52 | onAdd: PropTypes.func.isRequired, 53 | t: PropTypes.func.isRequired, 54 | } 55 | 56 | NewLane.defaultProps = {} 57 | 58 | export default NewLane 59 | -------------------------------------------------------------------------------- /src/components/NewLaneSection.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {NewLaneSection} from 'rt/styles/Base' 3 | import {AddLaneLink} from 'rt/styles/Elements' 4 | 5 | export default ({t, onClick}) => ( 6 | 7 | {t('Add another lane')} 8 | 9 | ) 10 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import LaneHeader from './Lane/LaneHeader' 2 | import LaneFooter from './Lane/LaneFooter' 3 | import Card from './Card' 4 | import Loader from './Loader' 5 | import NewLaneForm from './NewLaneForm' 6 | import NewCardForm from './NewCardForm' 7 | import AddCardLink from './AddCardLink' 8 | import NewLaneSection from './NewLaneSection' 9 | import {GlobalStyle, Section, BoardWrapper, ScrollableLane } from 'rt/styles/Base' 10 | 11 | export default { 12 | GlobalStyle, 13 | BoardWrapper, 14 | Loader, 15 | ScrollableLane, 16 | LaneHeader, 17 | LaneFooter, 18 | Section, 19 | NewLaneForm, 20 | NewLaneSection, 21 | NewCardForm, 22 | Card, 23 | AddCardLink, 24 | } 25 | -------------------------------------------------------------------------------- /src/controllers/Board.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import {Provider} from 'react-redux' 3 | import classNames from 'classnames' 4 | import {applyMiddleware, createStore} from 'redux' 5 | import logger from 'redux-logger' 6 | import uuidv1 from 'uuid/v1' 7 | import BoardContainer from './BoardContainer' 8 | import createTranslate from 'rt/helpers/createTranslate' 9 | import boardReducer from 'rt/reducers/BoardReducer' 10 | 11 | const middlewares = process.env.REDUX_LOGGING ? [logger] : [] 12 | 13 | export default class Board extends Component { 14 | constructor({id}) { 15 | super() 16 | this.store = this.getStore() 17 | this.id = id || uuidv1() 18 | } 19 | 20 | getStore = () => { 21 | //When you create multiple boards, unique stores are created for isolation 22 | return createStore(boardReducer, applyMiddleware(...middlewares)) 23 | } 24 | 25 | render() { 26 | const {id, className, components} = this.props 27 | const allClassNames = classNames('react-trello-board', className || '') 28 | return ( 29 | 30 | <> 31 | 32 | 37 | 38 | 39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/controllers/BoardContainer.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import {bindActionCreators} from 'redux' 3 | import {connect} from 'react-redux' 4 | import Container from 'rt/dnd/Container' 5 | import Draggable from 'rt/dnd/Draggable' 6 | import PropTypes from 'prop-types' 7 | import pick from 'lodash/pick' 8 | import isEqual from 'lodash/isEqual' 9 | import Lane from './Lane' 10 | import { PopoverWrapper } from 'react-popopo' 11 | 12 | import * as boardActions from 'rt/actions/BoardActions' 13 | import * as laneActions from 'rt/actions/LaneActions' 14 | 15 | class BoardContainer extends Component { 16 | state = { 17 | addLaneMode: false 18 | } 19 | 20 | componentDidMount() { 21 | const {actions, eventBusHandle} = this.props 22 | actions.loadBoard(this.props.data) 23 | if (eventBusHandle) { 24 | this.wireEventBus() 25 | } 26 | } 27 | 28 | UNSAFE_componentWillReceiveProps(nextProps) { 29 | // nextProps.data changes when external Board input props change and nextProps.reducerData changes due to event bus or UI changes 30 | const {data, reducerData, onDataChange} = this.props 31 | if (nextProps.reducerData && !isEqual(reducerData, nextProps.reducerData)) { 32 | onDataChange(nextProps.reducerData) 33 | } 34 | if (nextProps.data && !isEqual(nextProps.data, data)) { 35 | this.props.actions.loadBoard(nextProps.data) 36 | onDataChange(nextProps.data) 37 | } 38 | } 39 | 40 | onDragStart = ({payload}) => { 41 | const {handleLaneDragStart} = this.props 42 | handleLaneDragStart(payload.id) 43 | } 44 | 45 | onLaneDrop = ({removedIndex, addedIndex, payload}) => { 46 | const {actions, handleLaneDragEnd} = this.props 47 | if (removedIndex !== addedIndex) { 48 | actions.moveLane({oldIndex: removedIndex, newIndex: addedIndex}) 49 | handleLaneDragEnd(removedIndex, addedIndex, payload) 50 | } 51 | } 52 | getCardDetails = (laneId, cardIndex) => { 53 | return this.props.reducerData.lanes.find(lane => lane.id === laneId).cards[cardIndex] 54 | } 55 | getLaneDetails = index => { 56 | return this.props.reducerData.lanes[index] 57 | } 58 | 59 | wireEventBus = () => { 60 | const {actions, eventBusHandle} = this.props 61 | let eventBus = { 62 | publish: event => { 63 | switch (event.type) { 64 | case 'ADD_CARD': 65 | return actions.addCard({laneId: event.laneId, card: event.card}) 66 | case 'UPDATE_CARD': 67 | return actions.updateCard({laneId: event.laneId, card: event.card}) 68 | case 'REMOVE_CARD': 69 | return actions.removeCard({laneId: event.laneId, cardId: event.cardId}) 70 | case 'REFRESH_BOARD': 71 | return actions.loadBoard(event.data) 72 | case 'MOVE_CARD': 73 | return actions.moveCardAcrossLanes({ 74 | fromLaneId: event.fromLaneId, 75 | toLaneId: event.toLaneId, 76 | cardId: event.cardId, 77 | index: event.index 78 | }) 79 | case 'UPDATE_CARDS': 80 | return actions.updateCards({laneId: event.laneId, cards: event.cards}) 81 | case 'UPDATE_CARD': 82 | return actions.updateCard({laneId: event.laneId, updatedCard: event.card}) 83 | case 'UPDATE_LANES': 84 | return actions.updateLanes(event.lanes) 85 | case 'UPDATE_LANE': 86 | return actions.updateLane(event.lane) 87 | } 88 | } 89 | } 90 | eventBusHandle(eventBus) 91 | } 92 | 93 | // + add 94 | hideEditableLane = () => { 95 | this.setState({addLaneMode: false}) 96 | } 97 | 98 | showEditableLane = () => { 99 | this.setState({addLaneMode: true}) 100 | } 101 | 102 | addNewLane = params => { 103 | this.hideEditableLane() 104 | this.props.actions.addLane(params) 105 | this.props.onLaneAdd(params) 106 | } 107 | 108 | get groupName() { 109 | const {id} = this.props 110 | return `TrelloBoard${id}` 111 | } 112 | 113 | render() { 114 | const { 115 | id, 116 | components, 117 | reducerData, 118 | draggable, 119 | laneDraggable, 120 | laneDragClass, 121 | laneDropClass, 122 | style, 123 | onDataChange, 124 | onCardAdd, 125 | onCardUpdate, 126 | onCardClick, 127 | onBeforeCardDelete, 128 | onCardDelete, 129 | onLaneScroll, 130 | onLaneClick, 131 | onLaneAdd, 132 | onLaneDelete, 133 | onLaneUpdate, 134 | editable, 135 | canAddLanes, 136 | laneStyle, 137 | onCardMoveAcrossLanes, 138 | t, 139 | ...otherProps 140 | } = this.props 141 | 142 | const {addLaneMode} = this.state 143 | // Stick to whitelisting attributes to segregate board and lane props 144 | const passthroughProps = pick(this.props, [ 145 | 'onCardMoveAcrossLanes', 146 | 'onLaneScroll', 147 | 'onLaneDelete', 148 | 'onLaneUpdate', 149 | 'onCardClick', 150 | 'onBeforeCardDelete', 151 | 'onCardDelete', 152 | 'onCardAdd', 153 | 'onCardUpdate', 154 | 'onLaneClick', 155 | 'laneSortFunction', 156 | 'draggable', 157 | 'laneDraggable', 158 | 'cardDraggable', 159 | 'collapsibleLanes', 160 | 'canAddLanes', 161 | 'hideCardDeleteIcon', 162 | 'tagStyle', 163 | 'handleDragStart', 164 | 'handleDragEnd', 165 | 'cardDragClass', 166 | 'editLaneTitle', 167 | 't' 168 | ]) 169 | 170 | return ( 171 | 172 | 173 | this.getLaneDetails(index)} 181 | groupName={this.groupName}> 182 | {reducerData.lanes.map((lane, index) => { 183 | const {id, droppable, ...otherProps} = lane 184 | const laneToRender = ( 185 | 200 | ) 201 | return draggable && laneDraggable ? {laneToRender} : laneToRender 202 | })} 203 | 204 | 205 | {canAddLanes && ( 206 | 207 | {editable && !addLaneMode ? : ( 208 | addLaneMode && 209 | )} 210 | 211 | )} 212 | 213 | ) 214 | } 215 | } 216 | 217 | BoardContainer.propTypes = { 218 | id: PropTypes.string, 219 | components: PropTypes.object, 220 | actions: PropTypes.object, 221 | data: PropTypes.object.isRequired, 222 | reducerData: PropTypes.object, 223 | onDataChange: PropTypes.func, 224 | eventBusHandle: PropTypes.func, 225 | onLaneScroll: PropTypes.func, 226 | onCardClick: PropTypes.func, 227 | onBeforeCardDelete: PropTypes.func, 228 | onCardDelete: PropTypes.func, 229 | onCardAdd: PropTypes.func, 230 | onCardUpdate: PropTypes.func, 231 | onLaneAdd: PropTypes.func, 232 | onLaneDelete: PropTypes.func, 233 | onLaneClick: PropTypes.func, 234 | onLaneUpdate: PropTypes.func, 235 | laneSortFunction: PropTypes.func, 236 | draggable: PropTypes.bool, 237 | collapsibleLanes: PropTypes.bool, 238 | editable: PropTypes.bool, 239 | canAddLanes: PropTypes.bool, 240 | hideCardDeleteIcon: PropTypes.bool, 241 | handleDragStart: PropTypes.func, 242 | handleDragEnd: PropTypes.func, 243 | handleLaneDragStart: PropTypes.func, 244 | handleLaneDragEnd: PropTypes.func, 245 | style: PropTypes.object, 246 | tagStyle: PropTypes.object, 247 | laneDraggable: PropTypes.bool, 248 | cardDraggable: PropTypes.bool, 249 | cardDragClass: PropTypes.string, 250 | laneDragClass: PropTypes.string, 251 | laneDropClass: PropTypes.string, 252 | onCardMoveAcrossLanes: PropTypes.func.isRequired, 253 | t: PropTypes.func.isRequired, 254 | } 255 | 256 | BoardContainer.defaultProps = { 257 | t: v=>v, 258 | onDataChange: () => {}, 259 | handleDragStart: () => {}, 260 | handleDragEnd: () => {}, 261 | handleLaneDragStart: () => {}, 262 | handleLaneDragEnd: () => {}, 263 | onCardUpdate: () => {}, 264 | onLaneAdd: () => {}, 265 | onLaneDelete: () => {}, 266 | onCardMoveAcrossLanes: () => {}, 267 | onLaneUpdate: () => {}, 268 | editable: false, 269 | canAddLanes: false, 270 | hideCardDeleteIcon: false, 271 | draggable: false, 272 | collapsibleLanes: false, 273 | laneDraggable: true, 274 | cardDraggable: true, 275 | cardDragClass: 'react_trello_dragClass', 276 | laneDragClass: 'react_trello_dragLaneClass', 277 | laneDropClass: '' 278 | } 279 | 280 | const mapStateToProps = state => { 281 | return state.lanes ? {reducerData: state} : {} 282 | } 283 | 284 | const mapDispatchToProps = dispatch => ({actions: bindActionCreators({...boardActions, ...laneActions}, dispatch)}) 285 | 286 | export default connect( 287 | mapStateToProps, 288 | mapDispatchToProps 289 | )(BoardContainer) 290 | -------------------------------------------------------------------------------- /src/controllers/Lane.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import classNames from 'classnames' 3 | import PropTypes from 'prop-types' 4 | import {bindActionCreators} from 'redux' 5 | import {connect} from 'react-redux' 6 | import isEqual from 'lodash/isEqual' 7 | import cloneDeep from 'lodash/cloneDeep' 8 | import pick from 'lodash/pick' 9 | import uuidv1 from 'uuid/v1' 10 | 11 | import Container from 'rt/dnd/Container' 12 | import Draggable from 'rt/dnd/Draggable' 13 | 14 | import * as laneActions from 'rt/actions/LaneActions' 15 | 16 | class Lane extends Component { 17 | state = { 18 | loading: false, 19 | currentPage: this.props.currentPage, 20 | addCardMode: false, 21 | collapsed: false, 22 | isDraggingOver: false 23 | } 24 | 25 | handleScroll = evt => { 26 | const node = evt.target 27 | const elemScrollPosition = node.scrollHeight - node.scrollTop - node.clientHeight 28 | const {onLaneScroll} = this.props 29 | // In some browsers and/or screen sizes a decimal rest value between 0 and 1 exists, so it should be checked on < 1 instead of < 0 30 | if (elemScrollPosition < 1 && onLaneScroll && !this.state.loading) { 31 | const {currentPage} = this.state 32 | this.setState({loading: true}) 33 | const nextPage = currentPage + 1 34 | onLaneScroll(nextPage, this.props.id).then(moreCards => { 35 | if ((moreCards || []).length > 0) { 36 | this.props.actions.paginateLane({ 37 | laneId: this.props.id, 38 | newCards: moreCards, 39 | nextPage: nextPage 40 | }) 41 | } 42 | this.setState({loading: false}) 43 | }) 44 | } 45 | } 46 | 47 | sortCards(cards, sortFunction) { 48 | if (!cards) return [] 49 | if (!sortFunction) return cards 50 | return cards.concat().sort(function (card1, card2) { 51 | return sortFunction(card1, card2) 52 | }) 53 | } 54 | 55 | laneDidMount = node => { 56 | if (node) { 57 | node.addEventListener('scroll', this.handleScroll) 58 | } 59 | } 60 | 61 | UNSAFE_componentWillReceiveProps(nextProps) { 62 | if (!isEqual(this.props.cards, nextProps.cards)) { 63 | this.setState({ 64 | currentPage: nextProps.currentPage 65 | }) 66 | } 67 | } 68 | 69 | removeCard = cardId => { 70 | if (this.props.onBeforeCardDelete && typeof this.props.onBeforeCardDelete === 'function') { 71 | this.props.onBeforeCardDelete(() => { 72 | this.props.onCardDelete && this.props.onCardDelete(cardId, this.props.id) 73 | this.props.actions.removeCard({laneId: this.props.id, cardId: cardId}) 74 | }) 75 | } else { 76 | this.props.onCardDelete && this.props.onCardDelete(cardId, this.props.id) 77 | this.props.actions.removeCard({laneId: this.props.id, cardId: cardId}) 78 | } 79 | } 80 | 81 | handleCardClick = (e, card) => { 82 | const {onCardClick} = this.props 83 | onCardClick && onCardClick(card.id, card.metadata, card.laneId) 84 | e.stopPropagation() 85 | } 86 | 87 | showEditableCard = () => { 88 | this.setState({addCardMode: true}) 89 | } 90 | 91 | hideEditableCard = () => { 92 | this.setState({addCardMode: false}) 93 | } 94 | 95 | addNewCard = params => { 96 | const laneId = this.props.id 97 | const id = uuidv1() 98 | this.hideEditableCard() 99 | let card = {id, ...params} 100 | this.props.actions.addCard({laneId, card}) 101 | this.props.onCardAdd(card, laneId) 102 | } 103 | 104 | onDragStart = ({payload}) => { 105 | const {handleDragStart} = this.props 106 | handleDragStart && handleDragStart(payload.id, payload.laneId) 107 | } 108 | 109 | shouldAcceptDrop = sourceContainerOptions => { 110 | return this.props.droppable && sourceContainerOptions.groupName === this.groupName 111 | } 112 | 113 | get groupName() { 114 | const {boardId} = this.props 115 | return `TrelloBoard${boardId}Lane` 116 | } 117 | 118 | onDragEnd = (laneId, result) => { 119 | const {handleDragEnd} = this.props 120 | const {addedIndex, payload} = result 121 | 122 | if (this.state.isDraggingOver) { 123 | this.setState({isDraggingOver: false}) 124 | } 125 | 126 | if (addedIndex != null) { 127 | const newCard = {...cloneDeep(payload), laneId} 128 | const response = handleDragEnd ? handleDragEnd(payload.id, payload.laneId, laneId, addedIndex, newCard) : true 129 | if (response === undefined || !!response) { 130 | this.props.actions.moveCardAcrossLanes({ 131 | fromLaneId: payload.laneId, 132 | toLaneId: laneId, 133 | cardId: payload.id, 134 | index: addedIndex 135 | }) 136 | this.props.onCardMoveAcrossLanes(payload.laneId, laneId, payload.id, addedIndex) 137 | } 138 | return response 139 | } 140 | } 141 | 142 | updateCard = updatedCard => { 143 | this.props.actions.updateCard({laneId: this.props.id, card: updatedCard}) 144 | this.props.onCardUpdate(this.props.id, updatedCard) 145 | } 146 | 147 | renderDragContainer = isDraggingOver => { 148 | const { 149 | id, 150 | cards, 151 | laneSortFunction, 152 | editable, 153 | hideCardDeleteIcon, 154 | cardDraggable, 155 | cardDragClass, 156 | cardDropClass, 157 | tagStyle, 158 | cardStyle, 159 | components, 160 | t 161 | } = this.props 162 | const {addCardMode, collapsed} = this.state 163 | 164 | const showableCards = collapsed ? [] : cards 165 | 166 | const cardList = this.sortCards(showableCards, laneSortFunction).map((card, idx) => { 167 | const onDeleteCard = () => this.removeCard(card.id) 168 | const cardToRender = ( 169 | this.handleCardClick(e, card)} 176 | onChange={updatedCard => this.updateCard(updatedCard)} 177 | showDeleteButton={!hideCardDeleteIcon} 178 | tagStyle={tagStyle} 179 | cardDraggable={cardDraggable} 180 | editable={editable} 181 | t={t} 182 | {...card} 183 | /> 184 | ) 185 | return cardDraggable && (!card.hasOwnProperty('draggable') || card.draggable) ? ( 186 | {cardToRender} 187 | ) : ( 188 | {cardToRender} 189 | ) 190 | }) 191 | 192 | return ( 193 | 194 | this.onDragEnd(id, e)} 201 | onDragEnter={() => this.setState({isDraggingOver: true})} 202 | onDragLeave={() => this.setState({isDraggingOver: false})} 203 | shouldAcceptDrop={this.shouldAcceptDrop} 204 | getChildPayload={index => this.props.getCardDetails(id, index)}> 205 | {cardList} 206 | 207 | {editable && !addCardMode && } 208 | {addCardMode && ( 209 | 210 | )} 211 | 212 | ) 213 | } 214 | 215 | removeLane = () => { 216 | const {id} = this.props 217 | this.props.actions.removeLane({laneId: id}) 218 | this.props.onLaneDelete(id) 219 | } 220 | 221 | updateTitle = value => { 222 | this.props.actions.updateLane({id: this.props.id, title: value}) 223 | this.props.onLaneUpdate(this.props.id, {title: value}) 224 | } 225 | 226 | renderHeader = pickedProps => { 227 | const {components} = this.props 228 | return ( 229 | 235 | ) 236 | } 237 | 238 | toggleLaneCollapsed = () => { 239 | this.props.collapsibleLanes && this.setState(state => ({collapsed: !state.collapsed})) 240 | } 241 | 242 | render() { 243 | const {loading, isDraggingOver, collapsed} = this.state 244 | const { 245 | id, 246 | cards, 247 | collapsibleLanes, 248 | components, 249 | onLaneClick, 250 | onLaneScroll, 251 | onCardClick, 252 | onCardAdd, 253 | onBeforeCardDelete, 254 | onCardDelete, 255 | onLaneDelete, 256 | onLaneUpdate, 257 | onCardUpdate, 258 | onCardMoveAcrossLanes, 259 | ...otherProps 260 | } = this.props 261 | const allClassNames = classNames('react-trello-lane', this.props.className || '') 262 | const showFooter = collapsibleLanes && cards.length > 0 263 | return ( 264 | onLaneClick && onLaneClick(id)} 268 | draggable={false} 269 | className={allClassNames}> 270 | {this.renderHeader({id, cards, ...otherProps})} 271 | {this.renderDragContainer(isDraggingOver)} 272 | {loading && } 273 | {showFooter && } 274 | 275 | ) 276 | } 277 | } 278 | 279 | Lane.propTypes = { 280 | actions: PropTypes.object, 281 | id: PropTypes.string.isRequired, 282 | boardId: PropTypes.string, 283 | title: PropTypes.node, 284 | index: PropTypes.number, 285 | laneSortFunction: PropTypes.func, 286 | style: PropTypes.object, 287 | cardStyle: PropTypes.object, 288 | tagStyle: PropTypes.object, 289 | titleStyle: PropTypes.object, 290 | labelStyle: PropTypes.object, 291 | cards: PropTypes.array, 292 | label: PropTypes.string, 293 | currentPage: PropTypes.number, 294 | draggable: PropTypes.bool, 295 | collapsibleLanes: PropTypes.bool, 296 | droppable: PropTypes.bool, 297 | onCardMoveAcrossLanes: PropTypes.func, 298 | onCardClick: PropTypes.func, 299 | onBeforeCardDelete: PropTypes.func, 300 | onCardDelete: PropTypes.func, 301 | onCardAdd: PropTypes.func, 302 | onCardUpdate: PropTypes.func, 303 | onLaneDelete: PropTypes.func, 304 | onLaneUpdate: PropTypes.func, 305 | onLaneClick: PropTypes.func, 306 | onLaneScroll: PropTypes.func, 307 | editable: PropTypes.bool, 308 | laneDraggable: PropTypes.bool, 309 | cardDraggable: PropTypes.bool, 310 | cardDragClass: PropTypes.string, 311 | cardDropClass: PropTypes.string, 312 | canAddLanes: PropTypes.bool, 313 | t: PropTypes.func.isRequired 314 | } 315 | 316 | Lane.defaultProps = { 317 | style: {}, 318 | titleStyle: {}, 319 | labelStyle: {}, 320 | label: undefined, 321 | editable: false, 322 | onLaneUpdate: () => {}, 323 | onCardAdd: () => {}, 324 | onCardUpdate: () => {} 325 | } 326 | 327 | const mapDispatchToProps = dispatch => ({ 328 | actions: bindActionCreators(laneActions, dispatch) 329 | }) 330 | 331 | export default connect(null, mapDispatchToProps)(Lane) 332 | -------------------------------------------------------------------------------- /src/dnd/Container.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import ReactDOM from 'react-dom' 3 | import PropTypes from 'prop-types' 4 | import container, {dropHandlers} from 'trello-smooth-dnd' 5 | 6 | container.dropHandler = dropHandlers.reactDropHandler().handler; 7 | container.wrapChild = p => p; // dont wrap children they will already be wrapped 8 | 9 | class Container extends Component { 10 | constructor(props) { 11 | super(props); 12 | this.getContainerOptions = this.getContainerOptions.bind(this); 13 | this.setRef = this.setRef.bind(this); 14 | this.prevContainer = null; 15 | } 16 | 17 | componentDidMount() { 18 | this.containerDiv = this.containerDiv || ReactDOM.findDOMNode(this); 19 | this.prevContainer = this.containerDiv; 20 | this.container = container(this.containerDiv, this.getContainerOptions()); 21 | } 22 | 23 | componentWillUnmount() { 24 | this.container.dispose(); 25 | this.container = null; 26 | } 27 | 28 | componentDidUpdate() { 29 | this.containerDiv = this.containerDiv || ReactDOM.findDOMNode(this); 30 | if (this.containerDiv) { 31 | if (this.prevContainer && this.prevContainer !== this.containerDiv) { 32 | this.container.dispose(); 33 | this.container = container(this.containerDiv, this.getContainerOptions()); 34 | this.prevContainer = this.containerDiv; 35 | } 36 | } 37 | } 38 | 39 | render() { 40 | if (this.props.render) { 41 | return this.props.render(this.setRef); 42 | } else { 43 | return ( 44 |
45 | {this.props.children} 46 |
47 | ); 48 | } 49 | } 50 | 51 | setRef(element) { 52 | this.containerDiv = element; 53 | } 54 | 55 | getContainerOptions() { 56 | const functionProps = {}; 57 | 58 | if (this.props.onDragStart) { 59 | functionProps.onDragStart = (...p) => this.props.onDragStart(...p); 60 | } 61 | 62 | if (this.props.onDragEnd) { 63 | functionProps.onDragEnd = (...p) => this.props.onDragEnd(...p); 64 | } 65 | 66 | if (this.props.onDrop) { 67 | functionProps.onDrop = (...p) => this.props.onDrop(...p); 68 | } 69 | 70 | if (this.props.getChildPayload) { 71 | functionProps.getChildPayload = (...p) => this.props.getChildPayload(...p); 72 | } 73 | 74 | if (this.props.shouldAnimateDrop) { 75 | functionProps.shouldAnimateDrop = (...p) => this.props.shouldAnimateDrop(...p); 76 | } 77 | 78 | if (this.props.shouldAcceptDrop) { 79 | functionProps.shouldAcceptDrop = (...p) => this.props.shouldAcceptDrop(...p); 80 | } 81 | 82 | if (this.props.onDragEnter) { 83 | functionProps.onDragEnter = (...p) => this.props.onDragEnter(...p); 84 | } 85 | 86 | if (this.props.onDragLeave) { 87 | functionProps.onDragLeave = (...p) => this.props.onDragLeave(...p); 88 | } 89 | 90 | if (this.props.render) { 91 | functionProps.render = (...p) => this.props.render(...p); 92 | } 93 | 94 | if (this.props.onDropReady) { 95 | functionProps.onDropReady = (...p) => this.props.onDropReady(...p); 96 | } 97 | 98 | if (this.props.getGhostParent) { 99 | functionProps.getGhostParent = (...p) => this.props.getGhostParent(...p); 100 | } 101 | 102 | return Object.assign({}, this.props, functionProps); 103 | } 104 | } 105 | 106 | Container.propTypes = { 107 | behaviour: PropTypes.oneOf(["move", "copy", "drag-zone"]), 108 | groupName: PropTypes.string, 109 | orientation: PropTypes.oneOf(["horizontal", "vertical"]), 110 | style: PropTypes.object, 111 | dragHandleSelector: PropTypes.string, 112 | className: PropTypes.string, 113 | nonDragAreaSelector: PropTypes.string, 114 | dragBeginDelay: PropTypes.number, 115 | animationDuration: PropTypes.number, 116 | autoScrollEnabled: PropTypes.string, 117 | lockAxis: PropTypes.string, 118 | dragClass: PropTypes.string, 119 | dropClass: PropTypes.string, 120 | onDragStart: PropTypes.func, 121 | onDragEnd: PropTypes.func, 122 | onDrop: PropTypes.func, 123 | getChildPayload: PropTypes.func, 124 | shouldAnimateDrop: PropTypes.func, 125 | shouldAcceptDrop: PropTypes.func, 126 | onDragEnter: PropTypes.func, 127 | onDragLeave: PropTypes.func, 128 | render: PropTypes.func, 129 | getGhostParent: PropTypes.func, 130 | removeOnDropOut: PropTypes.bool 131 | }; 132 | 133 | Container.defaultProps = { 134 | behaviour: 'move', 135 | orientation: 'vertical', 136 | className: 'reactTrelloBoard' 137 | }; 138 | 139 | export default Container; 140 | -------------------------------------------------------------------------------- /src/dnd/Draggable.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import PropTypes from 'prop-types' 3 | import {constants} from 'trello-smooth-dnd' 4 | const {wrapperClass} = constants 5 | 6 | class Draggable extends Component { 7 | render() { 8 | if (this.props.render) { 9 | return React.cloneElement(this.props.render(), {className: wrapperClass}) 10 | } 11 | 12 | const clsName = `${this.props.className ? this.props.className + ' ' : ''}` 13 | return ( 14 |
15 | {this.props.children} 16 |
17 | ) 18 | } 19 | } 20 | 21 | Draggable.propTypes = { 22 | render: PropTypes.func 23 | } 24 | 25 | export default Draggable 26 | -------------------------------------------------------------------------------- /src/helpers/LaneHelper.js: -------------------------------------------------------------------------------- 1 | import update from 'immutability-helper' 2 | 3 | const LaneHelper = { 4 | initialiseLanes: (state, {lanes}) => { 5 | const newLanes = lanes.map(lane => { 6 | lane.currentPage = 1 7 | lane.cards && lane.cards.forEach(c => (c.laneId = lane.id)) 8 | return lane 9 | }) 10 | return update(state, {lanes: {$set: newLanes}}) 11 | }, 12 | 13 | paginateLane: (state, {laneId, newCards, nextPage}) => { 14 | const updatedLanes = LaneHelper.appendCardsToLane(state, {laneId: laneId, newCards: newCards}) 15 | updatedLanes.find(lane => lane.id === laneId).currentPage = nextPage 16 | return update(state, {lanes: {$set: updatedLanes}}) 17 | }, 18 | 19 | appendCardsToLane: (state, {laneId, newCards, index}) => { 20 | const lane = state.lanes.find(lane => lane.id === laneId) 21 | newCards = newCards 22 | .map(c => update(c, {laneId: {$set: laneId}})) 23 | .filter(c => lane.cards.find(card => card.id === c.id) == null) 24 | return state.lanes.map(lane => { 25 | if (lane.id === laneId) { 26 | if (index !== undefined) { 27 | return update(lane, {cards: {$splice: [[index, 0, ...newCards]]}}) 28 | } else { 29 | const cardsToUpdate = [...lane.cards, ...newCards] 30 | return update(lane, {cards: {$set: cardsToUpdate}}) 31 | } 32 | } else { 33 | return lane 34 | } 35 | }) 36 | }, 37 | 38 | appendCardToLane: (state, {laneId, card, index}) => { 39 | const newLanes = LaneHelper.appendCardsToLane(state, {laneId: laneId, newCards: [card], index}) 40 | return update(state, {lanes: {$set: newLanes}}) 41 | }, 42 | 43 | addLane: (state, lane) => { 44 | const newLane = {cards: [], ...lane} 45 | return update(state, {lanes: {$push: [newLane]}}) 46 | }, 47 | 48 | updateLane: (state, updatedLane) => { 49 | const newLanes = state.lanes.map(lane => { 50 | if (updatedLane.id == lane.id ) { 51 | return { ...lane, ...updatedLane } 52 | } else { 53 | return lane 54 | } 55 | }) 56 | return update(state, {lanes: {$set: newLanes}}) 57 | }, 58 | 59 | removeCardFromLane: (state, {laneId, cardId}) => { 60 | const lanes = state.lanes.map(lane => { 61 | if (lane.id === laneId) { 62 | let newCards = lane.cards.filter(card => card.id !== cardId) 63 | return update(lane, {cards: {$set: newCards}}) 64 | } else { 65 | return lane 66 | } 67 | }) 68 | return update(state, {lanes: {$set: lanes}}) 69 | }, 70 | 71 | moveCardAcrossLanes: (state, {fromLaneId, toLaneId, cardId, index}) => { 72 | let cardToMove = null 73 | const interimLanes = state.lanes.map(lane => { 74 | if (lane.id === fromLaneId) { 75 | cardToMove = lane.cards.find(card => card.id === cardId) 76 | const newCards = lane.cards.filter(card => card.id !== cardId) 77 | return update(lane, {cards: {$set: newCards}}) 78 | } else { 79 | return lane 80 | } 81 | }) 82 | const updatedState = update(state, {lanes: {$set: interimLanes}}) 83 | return LaneHelper.appendCardToLane(updatedState, {laneId: toLaneId, card: cardToMove, index: index}) 84 | }, 85 | 86 | updateCardsForLane: (state, {laneId, cards}) => { 87 | const lanes = state.lanes.map(lane => { 88 | if (lane.id === laneId) { 89 | return update(lane, {cards: {$set: cards}}) 90 | } else { 91 | return lane 92 | } 93 | }) 94 | return update(state, {lanes: {$set: lanes}}) 95 | }, 96 | 97 | updateCardForLane: (state, {laneId, card: updatedCard}) => { 98 | const lanes = state.lanes.map(lane => { 99 | if (lane.id === laneId) { 100 | const cards = lane.cards.map(card => { 101 | if (card.id === updatedCard.id) { 102 | return {...card, ...updatedCard} 103 | } else { 104 | return card 105 | } 106 | }) 107 | return update(lane, {cards: {$set: cards}}) 108 | } else { 109 | return lane 110 | } 111 | }) 112 | return update(state, {lanes: {$set: lanes}}) 113 | }, 114 | 115 | updateLanes: (state, lanes) => { 116 | return {...state, ...{lanes: lanes}} 117 | }, 118 | 119 | moveLane: (state, {oldIndex, newIndex}) => { 120 | const laneToMove = state.lanes[oldIndex] 121 | const tempState = update(state, {lanes: {$splice: [[oldIndex, 1]]}}); 122 | return update(tempState, {lanes: {$splice: [[newIndex, 0, laneToMove]]}}) 123 | }, 124 | 125 | removeLane: (state, {laneId}) => { 126 | const updatedLanes = state.lanes.filter(lane => lane.id !== laneId) 127 | return update(state, {lanes: {$set: updatedLanes}}) 128 | } 129 | } 130 | 131 | export default LaneHelper 132 | -------------------------------------------------------------------------------- /src/helpers/createTranslate.js: -------------------------------------------------------------------------------- 1 | import get from 'lodash/get' 2 | export default (TABLE) => (key) => get(TABLE, key) 3 | -------------------------------------------------------------------------------- /src/helpers/deprecationWarnings.js: -------------------------------------------------------------------------------- 1 | const REPLACE_TABLE = { 2 | addCardLink: 'components.Card', 3 | customLaneHeader: 'components.LaneHeader', 4 | newLaneTemplate: 'components.NewLaneSection', 5 | newCardTemplate: 'components.NewCardForm', 6 | children: 'components.Card', 7 | customCardLayout: 'components.Card', 8 | addLaneTitle: '`t` function with key "Add another lane"', 9 | addCardLink: '`t` function with key "Click to add card"', 10 | } 11 | 12 | const warn = (prop) => { 13 | const use = REPLACE_TABLE[prop] 14 | console.warn(`react-trello property '${prop}' is removed. Use '${use}' instead. More - https://github.com/rcdexta/react-trello/blob/master/UPGRADE.md`) 15 | } 16 | 17 | export default (props) => { 18 | Object.keys(REPLACE_TABLE).forEach((key) => { 19 | if (props.hasOwnProperty(key)) { 20 | warn(key) 21 | } 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Draggable from './dnd/Draggable' 4 | import Container from './dnd/Container' 5 | import BoardContainer from './controllers/BoardContainer' 6 | import Board from './controllers/Board' 7 | import Lane from './controllers/Lane' 8 | import deprecationWarnings from './helpers/deprecationWarnings' 9 | import DefaultComponents from './components' 10 | import locales from './locales' 11 | 12 | import widgets from './widgets' 13 | 14 | import createTranslate from './helpers/createTranslate' 15 | 16 | export { 17 | Draggable, 18 | Container, 19 | BoardContainer, 20 | Lane, 21 | createTranslate, 22 | locales, 23 | widgets 24 | } 25 | 26 | export { DefaultComponents as components } 27 | 28 | const DEFAULT_LANG='en' 29 | 30 | export default ({ components, lang=DEFAULT_LANG, ...otherProps }) => { 31 | deprecationWarnings(otherProps); 32 | const translate = createTranslate(locales[lang].translation) 33 | return 34 | } 35 | -------------------------------------------------------------------------------- /src/locales/en/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "Add another lane": "+ Add another lane", 3 | "Click to add card": "Click to add card", 4 | "Delete lane": "Delete lane", 5 | "Lane actions": "Lane actions", 6 | "button": { 7 | "Add lane": "Add lane", 8 | "Add card": "Add card", 9 | "Cancel": "Cancel" 10 | }, 11 | "placeholder": { 12 | "title": "title", 13 | "description": "description", 14 | "label": "label" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/locales/index.js: -------------------------------------------------------------------------------- 1 | // i18next support structure 2 | 3 | export default { 4 | "en": { 5 | translation: require('./en/translation.json') 6 | }, 7 | "ru": { 8 | translation: require('./ru/translation.json') 9 | }, 10 | "pt-br": { 11 | translation: require('./pt-br/translation.json') 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/locales/pt-br/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "Add another lane": "+ Adicionar outra coluna", 3 | "Click to add card": "Clique para adicionar um cartão", 4 | "Delete lane": "Deletar coluna", 5 | "Lane actions": "Ações da coluna", 6 | "button": { 7 | "Add lane": "Adicionar coluna", 8 | "Add card": "Adicionar cartão", 9 | "Cancel": "Cancelar" 10 | }, 11 | "placeholder": { 12 | "title": "título", 13 | "description": "descrição", 14 | "label": "etiqueta" 15 | } 16 | } -------------------------------------------------------------------------------- /src/locales/ru/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "Add another lane": "+Добавить колонку", 3 | "Click to add card": "+Добавить карточку", 4 | "Delete lane": "Удалить колонку", 5 | "Lane actions": "Действия над колонкой", 6 | "button": { 7 | "Add card": "Добавить карту", 8 | "Add lane": "Добавить колонку", 9 | "Cancel": "Отменить" 10 | }, 11 | "placeholder": { 12 | "title": "Название", 13 | "description": "Описание", 14 | "label": "Метка" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/reducers/BoardReducer.js: -------------------------------------------------------------------------------- 1 | import Lh from 'rt/helpers/LaneHelper' 2 | 3 | const boardReducer = (state = {lanes: []}, action) => { 4 | const {payload, type} = action 5 | switch (type) { 6 | case 'LOAD_BOARD': 7 | return Lh.initialiseLanes(state, payload) 8 | case 'ADD_CARD': 9 | return Lh.appendCardToLane(state, payload) 10 | case 'REMOVE_CARD': 11 | return Lh.removeCardFromLane(state, payload) 12 | case 'MOVE_CARD': 13 | return Lh.moveCardAcrossLanes(state, payload) 14 | case 'UPDATE_CARDS': 15 | return Lh.updateCardsForLane(state, payload) 16 | case 'UPDATE_CARD': 17 | return Lh.updateCardForLane(state, payload) 18 | case 'UPDATE_LANES': 19 | return Lh.updateLanes(state, payload) 20 | case 'UPDATE_LANE': 21 | return Lh.updateLane(state, payload) 22 | case 'PAGINATE_LANE': 23 | return Lh.paginateLane(state, payload) 24 | case 'MOVE_LANE': 25 | return Lh.moveLane(state, payload) 26 | case 'REMOVE_LANE': 27 | return Lh.removeLane(state, payload) 28 | case 'ADD_LANE': 29 | return Lh.addLane(state, payload) 30 | default: 31 | return state 32 | } 33 | } 34 | 35 | export default boardReducer 36 | -------------------------------------------------------------------------------- /src/styles/Base.js: -------------------------------------------------------------------------------- 1 | import {PopoverContainer, PopoverContent} from 'react-popopo' 2 | import styled, {createGlobalStyle, css} from 'styled-components' 3 | 4 | export const GlobalStyle = createGlobalStyle` 5 | .comPlainTextContentEditable { 6 | -webkit-user-modify: read-write-plaintext-only; 7 | cursor: text; 8 | } 9 | 10 | .comPlainTextContentEditable--has-placeholder::before { 11 | content: attr(placeholder); 12 | opacity: 0.5; 13 | color: inherit; 14 | cursor: text; 15 | } 16 | 17 | .react_trello_dragClass { 18 | transform: rotate(3deg); 19 | } 20 | 21 | .react_trello_dragLaneClass { 22 | transform: rotate(3deg); 23 | } 24 | 25 | .icon-overflow-menu-horizontal:before { 26 | content: "\\E91F"; 27 | } 28 | .icon-lg, .icon-sm { 29 | color: #798d99; 30 | } 31 | .icon-lg { 32 | height: 32px; 33 | font-size: 16px; 34 | line-height: 32px; 35 | width: 32px; 36 | } 37 | ` 38 | 39 | export const CustomPopoverContainer = styled(PopoverContainer)` 40 | position: absolute; 41 | right: 10px; 42 | flex-flow: column nowrap; 43 | ` 44 | 45 | export const CustomPopoverContent = styled(PopoverContent)` 46 | visibility: hidden; 47 | margin-top: -5px; 48 | opacity: 0; 49 | position: absolute; 50 | z-index: 10; 51 | box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.3); 52 | transition: all 0.3s ease 0ms; 53 | border-radius: 3px; 54 | min-width: 7em; 55 | flex-flow: column nowrap; 56 | background-color: #fff; 57 | color: #000; 58 | padding: 5px; 59 | left: 50%; 60 | transform: translateX(-50%); 61 | ${props => 62 | props.active && 63 | ` 64 | visibility: visible; 65 | opacity: 1; 66 | transition-delay: 100ms; 67 | `} &::before { 68 | visibility: hidden; 69 | } 70 | a { 71 | color: rgba(255, 255, 255, 0.56); 72 | padding: 0.5em 1em; 73 | margin: 0; 74 | text-decoration: none; 75 | &:hover { 76 | background-color: #00bcd4 !important; 77 | color: #37474f; 78 | } 79 | } 80 | ` 81 | 82 | export const BoardWrapper = styled.div` 83 | background-color: #3179ba; 84 | overflow-y: hidden; 85 | padding: 5px; 86 | color: #393939; 87 | display: flex; 88 | flex-direction: row; 89 | align-items: flex-start; 90 | height: 100vh; 91 | ` 92 | 93 | export const Header = styled.header` 94 | margin-bottom: 10px; 95 | display: flex; 96 | flex-direction: row; 97 | align-items: flex-start; 98 | ` 99 | 100 | export const Section = styled.section` 101 | background-color: #e3e3e3; 102 | border-radius: 3px; 103 | margin: 5px 5px; 104 | position: relative; 105 | padding: 10px; 106 | display: inline-flex; 107 | height: auto; 108 | max-height: 90%; 109 | flex-direction: column; 110 | ` 111 | 112 | export const LaneHeader = styled(Header)` 113 | margin-bottom: 0px; 114 | ${props => 115 | props.editLaneTitle && 116 | css` 117 | padding: 0px; 118 | line-height: 30px; 119 | `} ${props => 120 | !props.editLaneTitle && 121 | css` 122 | padding: 0px 5px; 123 | `}; 124 | ` 125 | 126 | export const LaneFooter = styled.div` 127 | display: flex; 128 | justify-content: center; 129 | align-items: center; 130 | width: 100%; 131 | position: relative; 132 | height: 10px; 133 | ` 134 | 135 | export const ScrollableLane = styled.div` 136 | flex: 1; 137 | overflow-y: auto; 138 | min-width: 250px; 139 | overflow-x: hidden; 140 | align-self: center; 141 | max-height: 90vh; 142 | margin-top: 10px; 143 | flex-direction: column; 144 | justify-content: space-between; 145 | ` 146 | 147 | export const Title = styled.span` 148 | font-weight: bold; 149 | font-size: 15px; 150 | line-height: 18px; 151 | cursor: ${props => (props.draggable ? 'grab' : `auto`)}; 152 | width: 70%; 153 | ` 154 | 155 | export const RightContent = styled.span` 156 | width: 38%; 157 | text-align: right; 158 | padding-right: 10px; 159 | font-size: 13px; 160 | ` 161 | export const CardWrapper = styled.article` 162 | border-radius: 3px; 163 | border-bottom: 1px solid #ccc; 164 | background-color: #fff; 165 | position: relative; 166 | padding: 10px; 167 | cursor: pointer; 168 | max-width: 250px; 169 | margin-bottom: 7px; 170 | min-width: 230px; 171 | ` 172 | 173 | export const MovableCardWrapper = styled(CardWrapper)` 174 | &:hover { 175 | background-color: #f0f0f0; 176 | color: #000; 177 | } 178 | ` 179 | 180 | export const CardHeader = styled(Header)` 181 | border-bottom: 1px solid #eee; 182 | padding-bottom: 6px; 183 | color: #000; 184 | ` 185 | 186 | export const CardTitle = styled(Title)` 187 | font-size: 14px; 188 | ` 189 | 190 | export const CardRightContent = styled(RightContent)` 191 | font-size: 10px; 192 | ` 193 | 194 | export const Detail = styled.div` 195 | font-size: 12px; 196 | color: #4d4d4d; 197 | white-space: pre-wrap; 198 | ` 199 | 200 | export const Footer = styled.div` 201 | border-top: 1px solid #eee; 202 | padding-top: 6px; 203 | text-align: right; 204 | display: flex; 205 | justify-content: flex-end; 206 | flex-direction: row; 207 | flex-wrap: wrap; 208 | ` 209 | 210 | export const TagSpan = styled.span` 211 | padding: 2px 3px; 212 | border-radius: 3px; 213 | margin: 2px 5px; 214 | font-size: 70%; 215 | ` 216 | 217 | export const AddCardLink = styled.a` 218 | border-radius: 0 0 3px 3px; 219 | color: #838c91; 220 | display: block; 221 | padding: 5px 2px; 222 | margin-top: 10px; 223 | position: relative; 224 | text-decoration: none; 225 | cursor: pointer; 226 | 227 | &:hover { 228 | //background-color: #cdd2d4; 229 | color: #4d4d4d; 230 | text-decoration: underline; 231 | } 232 | ` 233 | 234 | export const LaneTitle = styled.div` 235 | font-size: 15px; 236 | width: 268px; 237 | height: auto; 238 | ` 239 | 240 | export const LaneSection = styled.section` 241 | background-color: #2b6aa3; 242 | border-radius: 3px; 243 | margin: 5px; 244 | position: relative; 245 | padding: 5px; 246 | display: inline-flex; 247 | height: auto; 248 | flex-direction: column; 249 | ` 250 | 251 | export const NewLaneSection = styled(LaneSection)` 252 | width: 200px; 253 | ` 254 | 255 | export const NewLaneButtons = styled.div` 256 | margin-top: 10px; 257 | ` 258 | 259 | export const CardForm = styled.div` 260 | background-color: #e3e3e3; 261 | ` 262 | 263 | export const InlineInput = styled.textarea` 264 | overflow-x: hidden; /* for Firefox (issue #5) */ 265 | word-wrap: break-word; 266 | min-height: 18px; 267 | max-height: 112px; /* optional, but recommended */ 268 | resize: none; 269 | width: 100%; 270 | height: 18px; 271 | font-size: inherit; 272 | font-weight: inherit; 273 | line-height: inherit; 274 | text-align: inherit; 275 | background-color: transparent; 276 | box-shadow: none; 277 | box-sizing: border-box; 278 | border-radius: 3px; 279 | border: 0; 280 | padding: 0 8px; 281 | outline: 0; 282 | ${props => 283 | props.border && 284 | css` 285 | &:focus { 286 | box-shadow: inset 0 0 0 2px #0079bf; 287 | } 288 | `} &:focus { 289 | background-color: white; 290 | } 291 | ` 292 | -------------------------------------------------------------------------------- /src/styles/Elements.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import {CardWrapper, MovableCardWrapper} from './Base' 3 | 4 | export const DeleteWrapper = styled.div` 5 | text-align: center; 6 | position: absolute; 7 | top: -1px; 8 | right: 2px; 9 | cursor: pointer; 10 | ` 11 | 12 | export const GenDelButton = styled.button` 13 | transition: all 0.5s ease; 14 | display: inline-block; 15 | border: none; 16 | font-size: 15px; 17 | height: 15px; 18 | padding: 0; 19 | margin-top: 5px; 20 | text-align: center; 21 | width: 15px; 22 | background: inherit; 23 | cursor: pointer; 24 | ` 25 | 26 | export const DelButton = styled.button` 27 | transition: all 0.5s ease; 28 | display: inline-block; 29 | border: none; 30 | font-size: 8px; 31 | height: 15px; 32 | line-height: 1px; 33 | margin: 0 0 8px; 34 | padding: 0; 35 | text-align: center; 36 | width: 15px; 37 | background: inherit; 38 | cursor: pointer; 39 | opacity: 0; 40 | ${MovableCardWrapper}:hover & { 41 | opacity: 1; 42 | } 43 | ` 44 | 45 | export const MenuButton = styled.button` 46 | transition: all 0.5s ease; 47 | display: inline-block; 48 | border: none; 49 | outline: none; 50 | font-size: 16px; 51 | font-weight: bold; 52 | height: 15px; 53 | line-height: 1px; 54 | margin: 0 0 8px; 55 | padding: 0; 56 | text-align: center; 57 | width: 15px; 58 | background: inherit; 59 | cursor: pointer; 60 | ` 61 | 62 | export const LaneMenuHeader = styled.div` 63 | position: relative; 64 | margin-bottom: 4px; 65 | text-align: center; 66 | ` 67 | 68 | export const LaneMenuContent = styled.div` 69 | overflow-x: hidden; 70 | overflow-y: auto; 71 | padding: 0 12px 12px; 72 | ` 73 | 74 | export const LaneMenuItem = styled.div` 75 | cursor: pointer; 76 | display: block; 77 | font-weight: 700; 78 | padding: 6px 12px; 79 | position: relative; 80 | margin: 0 -12px; 81 | text-decoration: none; 82 | 83 | &:hover { 84 | background-color: #3179BA; 85 | color: #fff; 86 | } 87 | ` 88 | 89 | export const LaneMenuTitle = styled.span` 90 | box-sizing: border-box; 91 | color: #6b808c; 92 | display: block; 93 | line-height: 30px; 94 | border-bottom: 1px solid rgba(9,45,66,.13); 95 | margin: 0 6px; 96 | overflow: hidden; 97 | padding: 0 32px; 98 | position: relative; 99 | text-overflow: ellipsis; 100 | white-space: nowrap; 101 | z-index: 1; 102 | ` 103 | 104 | export const DeleteIcon = styled.span` 105 | position: relative; 106 | display: inline-block; 107 | width: 4px; 108 | height: 4px; 109 | opacity: 1; 110 | overflow: hidden; 111 | border: 1px solid #83bd42; 112 | border-radius: 50%; 113 | padding: 4px; 114 | background-color: #83bd42; 115 | 116 | ${CardWrapper}:hover & { 117 | opacity: 1; 118 | } 119 | 120 | &:hover::before, 121 | &:hover::after { 122 | background: red; 123 | } 124 | 125 | &:before, 126 | &:after { 127 | content: ''; 128 | position: absolute; 129 | height: 2px; 130 | width: 60%; 131 | top: 45%; 132 | left: 20%; 133 | background: #fff; 134 | border-radius: 5px; 135 | } 136 | 137 | &:before { 138 | -webkit-transform: rotate(45deg); 139 | -moz-transform: rotate(45deg); 140 | -o-transform: rotate(45deg); 141 | transform: rotate(45deg); 142 | } 143 | 144 | &:after { 145 | -webkit-transform: rotate(-45deg); 146 | -moz-transform: rotate(-45deg); 147 | -o-transform: rotate(-45deg); 148 | transform: rotate(-45deg); 149 | } 150 | ` 151 | 152 | export const ExpandCollapseBase = styled.span` 153 | width: 36px; 154 | margin: 0 auto; 155 | font-size: 14px; 156 | position: relative; 157 | cursor: pointer; 158 | ` 159 | 160 | export const CollapseBtn = styled(ExpandCollapseBase)` 161 | &:before { 162 | content: ''; 163 | position: absolute; 164 | top: 0; 165 | left: 0; 166 | border-bottom: 7px solid #444; 167 | border-left: 7px solid transparent; 168 | border-right: 7px solid transparent; 169 | border-radius: 6px; 170 | } 171 | &:after { 172 | content: ''; 173 | position: absolute; 174 | left: 4px; 175 | top: 4px; 176 | border-bottom: 3px solid #e3e3e3; 177 | border-left: 3px solid transparent; 178 | border-right: 3px solid transparent; 179 | } 180 | ` 181 | 182 | export const ExpandBtn = styled(ExpandCollapseBase)` 183 | &:before { 184 | content: ''; 185 | position: absolute; 186 | top: 0; 187 | left: 0; 188 | border-top: 7px solid #444; 189 | border-left: 7px solid transparent; 190 | border-right: 7px solid transparent; 191 | border-radius: 6px; 192 | } 193 | &:after { 194 | content: ''; 195 | position: absolute; 196 | left: 4px; 197 | top: 0px; 198 | border-top: 3px solid #e3e3e3; 199 | border-left: 3px solid transparent; 200 | border-right: 3px solid transparent; 201 | } 202 | ` 203 | 204 | export const AddButton = styled.button` 205 | background: #5aac44; 206 | color: #fff; 207 | transition: background 0.3s ease; 208 | min-height: 32px; 209 | padding: 4px 16px; 210 | vertical-align: top; 211 | margin-top: 0; 212 | margin-right: 8px; 213 | font-weight: bold; 214 | border-radius: 3px; 215 | font-size: 14px; 216 | cursor: pointer; 217 | margin-bottom: 0; 218 | ` 219 | 220 | export const CancelButton = styled.button` 221 | background: #999999; 222 | color: #fff; 223 | transition: background 0.3s ease; 224 | min-height: 32px; 225 | padding: 4px 16px; 226 | vertical-align: top; 227 | margin-top: 0; 228 | font-weight: bold; 229 | border-radius: 3px; 230 | font-size: 14px; 231 | cursor: pointer; 232 | margin-bottom: 0; 233 | ` 234 | export const AddLaneLink = styled.button` 235 | background: #2b6aa3; 236 | border: none; 237 | color: #fff; 238 | transition: background 0.3s ease; 239 | min-height: 32px; 240 | padding: 4px 16px; 241 | vertical-align: top; 242 | margin-top: 0; 243 | margin-right: 0px; 244 | border-radius: 4px; 245 | font-size: 13px; 246 | cursor: pointer; 247 | margin-bottom: 0; 248 | ` 249 | -------------------------------------------------------------------------------- /src/styles/Loader.js: -------------------------------------------------------------------------------- 1 | import styled, {keyframes} from 'styled-components' 2 | 3 | const keyframeAnimation = keyframes` 4 | 0% { 5 | transform: scale(1); 6 | } 7 | 20% { 8 | transform: scale(1, 2.2); 9 | } 10 | 40% { 11 | transform: scale(1); 12 | } 13 | ` 14 | export const LoaderDiv = styled.div` 15 | text-align: center; 16 | margin: 15px 0; 17 | ` 18 | 19 | export const LoadingBar = styled.div` 20 | display: inline-block; 21 | margin: 0 2px; 22 | width: 4px; 23 | height: 18px; 24 | border-radius: 4px; 25 | animation: ${keyframeAnimation} 1s ease-in-out infinite; 26 | background-color: #777; 27 | 28 | &:nth-child(1) { 29 | animation-delay: 0.0001s; 30 | } 31 | &:nth-child(2) { 32 | animation-delay: 0.09s; 33 | } 34 | &:nth-child(3) { 35 | animation-delay: 0.18s; 36 | } 37 | &:nth-child(4) { 38 | animation-delay: 0.27s; 39 | } 40 | ` 41 | -------------------------------------------------------------------------------- /src/widgets/DeleteButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {DeleteWrapper, DelButton} from 'rt/styles/Elements' 3 | 4 | const DeleteButton = props => { 5 | return ( 6 | 7 | 8 | 9 | ) 10 | } 11 | 12 | export default DeleteButton 13 | -------------------------------------------------------------------------------- /src/widgets/EditableLabel.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | class EditableLabel extends React.Component { 5 | constructor({value}) { 6 | super() 7 | this.state = { value: value } 8 | } 9 | 10 | getText = el => { 11 | return el.innerText 12 | } 13 | 14 | onTextChange = ev => { 15 | const value = this.getText(ev.target) 16 | this.setState({value: value}) 17 | } 18 | 19 | componentDidMount() { 20 | if (this.props.autoFocus) { 21 | this.refDiv.focus() 22 | } 23 | } 24 | 25 | onBlur = () => { 26 | this.props.onChange(this.state.value) 27 | } 28 | 29 | onPaste = ev => { 30 | ev.preventDefault() 31 | const value = ev.clipboardData.getData('text') 32 | document.execCommand('insertText', false, value) 33 | } 34 | 35 | getClassName = () => { 36 | const placeholder = this.state.value === '' ? 'comPlainTextContentEditable--has-placeholder' : '' 37 | return `comPlainTextContentEditable ${placeholder}` 38 | } 39 | 40 | onKeyDown = (e) => { 41 | if(e.keyCode === 13) { 42 | this.props.onChange(this.state.value) 43 | this.refDiv.blur() 44 | e.preventDefault() 45 | } 46 | if(e.keyCode === 27) { 47 | this.refDiv.value = this.props.value 48 | this.setState({value: this.props.value}) 49 | // this.refDiv.blur() 50 | e.preventDefault() 51 | e.stopPropagation() 52 | } 53 | } 54 | 55 | render() { 56 | const placeholder = this.props.value.length > 0 ? false : this.props.placeholder; 57 | return ( 58 |
(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 |
28 |
{props.name}
29 |
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 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
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 |
53 |
54 | this.updateField('title', evt)} placeholder="Title" /> 55 |
56 |
57 | this.updateField('description', evt)} placeholder="Description" /> 58 |
59 |
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 | --------------------------------------------------------------------------------