329 lines
9.7 KiB
JavaScript
329 lines
9.7 KiB
JavaScript
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 = (
|
|
<components.Card
|
|
key={card.id}
|
|
index={idx}
|
|
style={card.style || cardStyle}
|
|
className="react-trello-card"
|
|
onDelete={onDeleteCard}
|
|
onClick={(e) => 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) ? (
|
|
<Draggable key={card.id}>{cardToRender}</Draggable>
|
|
) : (
|
|
<span key={card.id}>{cardToRender}</span>
|
|
);
|
|
});
|
|
|
|
return (
|
|
<components.ScrollableLane ref={this.laneDidMount} isDraggingOver={isDraggingOver}>
|
|
<Container
|
|
orientation="vertical"
|
|
groupName={this.groupName}
|
|
dragClass={cardDragClass}
|
|
dropClass={cardDropClass}
|
|
onDragStart={this.onDragStart}
|
|
onDrop={(e) => 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}
|
|
</Container>
|
|
{editable && !addCardMode && <components.AddCardLink onClick={this.showEditableCard} laneId={id} />}
|
|
{addCardMode && <components.NewCardForm onCancel={this.hideEditableCard} laneId={id} onAdd={this.addNewCard} />}
|
|
</components.ScrollableLane>
|
|
);
|
|
};
|
|
|
|
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 (
|
|
<components.LaneHeader
|
|
{...pickedProps}
|
|
onDelete={this.removeLane}
|
|
onDoubleClick={this.toggleLaneCollapsed}
|
|
updateTitle={this.updateTitle}
|
|
/>
|
|
);
|
|
};
|
|
|
|
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 (
|
|
<components.Section
|
|
{...otherProps}
|
|
key={id}
|
|
onClick={() => onLaneClick && onLaneClick(id)}
|
|
draggable={false}
|
|
className={allClassNames}
|
|
>
|
|
{this.renderHeader({ id, cards, ...otherProps })}
|
|
{this.renderDragContainer(isDraggingOver)}
|
|
{loading && <components.Loader />}
|
|
{showFooter && <components.LaneFooter onClick={this.toggleLaneCollapsed} collapsed={collapsed} />}
|
|
</components.Section>
|
|
);
|
|
}
|
|
}
|
|
|
|
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);
|