import React, { Component } from "react"; import classNames from "classnames"; import PropTypes from "prop-types"; import { bindActionCreators } from "redux"; import { connect } from "react-redux"; import isEqual from "lodash/isEqual"; import cloneDeep from "lodash/cloneDeep"; import { v1 } from "uuid"; import Container from "../dnd/Container.jsx"; import Draggable from "../dnd/Draggable.jsx"; import * as actions from "../../../redux/trello/trello.actions.js"; class Lane extends Component { state = { loading: false, currentPage: this.props.currentPage, addCardMode: false, collapsed: false, isDraggingOver: false }; get groupName() { const { boardId } = this.props; return `TrelloBoard${boardId}Lane`; } handleScroll = (evt) => { const node = evt.target; const elemScrollPosition = node.scrollHeight - node.scrollTop - node.clientHeight; const { onLaneScroll } = this.props; // 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 if (elemScrollPosition < 1 && onLaneScroll && !this.state.loading) { const { currentPage } = this.state; this.setState({ loading: true }); const nextPage = currentPage + 1; onLaneScroll(nextPage, this.props.id).then((moreCards) => { if ((moreCards || []).length > 0) { this.props.actions.paginateLane({ laneId: this.props.id, newCards: moreCards, nextPage: nextPage }); } this.setState({ loading: false }); }); } }; sortCards(cards, sortFunction) { if (!cards) return []; if (!sortFunction) return cards; return cards.concat().sort(function (card1, card2) { return sortFunction(card1, card2); }); } laneDidMount = (node) => { if (node) { node.addEventListener("scroll", this.handleScroll); } }; UNSAFE_componentWillReceiveProps(nextProps) { if (!isEqual(this.props.cards, nextProps.cards)) { this.setState({ currentPage: nextProps.currentPage }); } } removeCard = (cardId) => { if (this.props.onBeforeCardDelete && typeof this.props.onBeforeCardDelete === "function") { this.props.onBeforeCardDelete(() => { this.props.onCardDelete && this.props.onCardDelete(cardId, this.props.id); this.props.actions.removeCard({ laneId: this.props.id, cardId: cardId }); }); } else { this.props.onCardDelete && this.props.onCardDelete(cardId, this.props.id); this.props.actions.removeCard({ laneId: this.props.id, cardId: cardId }); } }; handleCardClick = (e, card) => { const { onCardClick } = this.props; onCardClick && onCardClick(card.id, card.metadata, card.laneId); e.stopPropagation(); }; showEditableCard = () => { this.setState({ addCardMode: true }); }; hideEditableCard = () => { this.setState({ addCardMode: false }); }; addNewCard = (params) => { const laneId = this.props.id; const id = v1(); this.hideEditableCard(); let card = { id, ...params }; this.props.actions.addCard({ laneId, card }); this.props.onCardAdd(card, laneId); }; onDragStart = ({ payload }) => { const { handleDragStart } = this.props; handleDragStart && handleDragStart(payload.id, payload.laneId); }; shouldAcceptDrop = (sourceContainerOptions) => { return this.props.droppable && sourceContainerOptions.groupName === this.groupName; }; onDragEnd = (laneId, result) => { const { handleDragEnd } = this.props; const { addedIndex, payload } = result; if (this.state.isDraggingOver) { this.setState({ isDraggingOver: false }); } if (addedIndex != null) { const newCard = { ...cloneDeep(payload), laneId }; const response = handleDragEnd ? handleDragEnd(payload.id, payload.laneId, laneId, addedIndex, newCard) : true; if (response === undefined || !!response) { this.props.actions.moveCardAcrossLanes({ fromLaneId: payload.laneId, toLaneId: laneId, cardId: payload.id, index: addedIndex }); this.props.onCardMoveAcrossLanes(payload.laneId, laneId, payload.id, addedIndex); } return response; } }; updateCard = (updatedCard) => { this.props.actions.updateCard({ laneId: this.props.id, card: updatedCard }); this.props.onCardUpdate(this.props.id, updatedCard); }; renderDragContainer = (isDraggingOver) => { const { id, cards, laneSortFunction, editable, hideCardDeleteIcon, cardDraggable, cardDragClass, cardDropClass, tagStyle, cardStyle, components, t } = this.props; const { addCardMode, collapsed } = this.state; const showableCards = collapsed ? [] : cards; const cardList = this.sortCards(showableCards, laneSortFunction).map((card, idx) => { const onDeleteCard = () => this.removeCard(card.id); const cardToRender = ( this.handleCardClick(e, card)} onChange={(updatedCard) => this.updateCard(updatedCard)} showDeleteButton={!hideCardDeleteIcon} tagStyle={tagStyle} cardDraggable={cardDraggable} editable={editable} {...card} /> ); return cardDraggable && (!card.hasOwnProperty("draggable") || card.draggable) ? ( {cardToRender} ) : ( {cardToRender} ); }); return ( this.onDragEnd(id, e)} onDragEnter={() => this.setState({ isDraggingOver: true })} onDragLeave={() => this.setState({ isDraggingOver: false })} shouldAcceptDrop={this.shouldAcceptDrop} getChildPayload={(index) => this.props.getCardDetails(id, index)} > {cardList} {editable && !addCardMode && } {addCardMode && } ); }; removeLane = () => { const { id } = this.props; this.props.actions.removeLane({ laneId: id }); this.props.onLaneDelete(id); }; updateTitle = (value) => { this.props.actions.updateLane({ id: this.props.id, title: value }); this.props.onLaneUpdate(this.props.id, { title: value }); }; renderHeader = (pickedProps) => { const { components } = this.props; return ( ); }; toggleLaneCollapsed = () => { this.props.collapsibleLanes && this.setState((state) => ({ collapsed: !state.collapsed })); }; render() { const { loading, isDraggingOver, collapsed } = this.state; const { id, cards, collapsibleLanes, components, onLaneClick, onLaneScroll, onCardClick, onCardAdd, onBeforeCardDelete, onCardDelete, onLaneDelete, onLaneUpdate, onCardUpdate, onCardMoveAcrossLanes, ...otherProps } = this.props; const allClassNames = classNames("react-trello-lane", this.props.className || ""); const showFooter = collapsibleLanes && cards.length > 0; return ( onLaneClick && onLaneClick(id)} draggable={false} className={allClassNames} > {this.renderHeader({ id, cards, ...otherProps })} {this.renderDragContainer(isDraggingOver)} {loading && } {showFooter && } ); } } Lane.propTypes = { actions: PropTypes.object, id: PropTypes.string.isRequired, boardId: PropTypes.string, title: PropTypes.node, index: PropTypes.number, laneSortFunction: PropTypes.func, style: PropTypes.object, cardStyle: PropTypes.object, tagStyle: PropTypes.object, titleStyle: PropTypes.object, labelStyle: PropTypes.object, cards: PropTypes.array, label: PropTypes.string, currentPage: PropTypes.number, draggable: PropTypes.bool, collapsibleLanes: PropTypes.bool, droppable: PropTypes.bool, onCardMoveAcrossLanes: PropTypes.func, onCardClick: PropTypes.func, onBeforeCardDelete: PropTypes.func, onCardDelete: PropTypes.func, onCardAdd: PropTypes.func, onCardUpdate: PropTypes.func, onLaneDelete: PropTypes.func, onLaneUpdate: PropTypes.func, onLaneClick: PropTypes.func, onLaneScroll: PropTypes.func, editable: PropTypes.bool, laneDraggable: PropTypes.bool, cardDraggable: PropTypes.bool, cardDragClass: PropTypes.string, cardDropClass: PropTypes.string, canAddLanes: PropTypes.bool }; Lane.defaultProps = { style: {}, titleStyle: {}, labelStyle: {}, label: undefined, editable: false, onLaneUpdate: () => {}, onCardAdd: () => {}, onCardUpdate: () => {} }; const mapDispatchToProps = (dispatch) => ({ actions: bindActionCreators(actions, dispatch) }); export default connect(null, mapDispatchToProps)(Lane);