Introduce React-Trello in place of React-Kanban

Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
Dave Richer
2024-05-09 13:22:58 -04:00
parent f77a16648f
commit f647e1ff11
49 changed files with 2632 additions and 119 deletions

View File

@@ -0,0 +1,19 @@
import { BoardContainer } from "../index.jsx";
import classNames from "classnames";
import { useState } from "react";
import { v1 } from "uuid";
const Board = ({ id, className, components, ...additionalProps }) => {
const [storeId] = useState(id || v1());
const allClassNames = classNames("react-trello-board", className || "");
return (
<>
<components.GlobalStyle />
<BoardContainer components={components} {...additionalProps} id={storeId} className={allClassNames} />
</>
);
};
export default Board;

View File

@@ -0,0 +1,294 @@
import React, { Component } from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import Container from "../dnd/Container";
import Draggable from "../dnd/Draggable";
import PropTypes from "prop-types";
import pick from "lodash/pick";
import isEqual from "lodash/isEqual";
import Lane from "./Lane";
import { PopoverWrapper } from "react-popopo";
import * as actions from "../../../redux/trello/trello.actions.js";
class BoardContainer extends Component {
state = {
addLaneMode: false
};
get groupName() {
const { id } = this.props;
return `TrelloBoard${id}`;
}
componentDidMount() {
const { actions, eventBusHandle } = this.props;
actions.loadBoard(this.props.data);
if (eventBusHandle) {
this.wireEventBus();
}
}
componentDidUpdate(prevProps) {
const { data, reducerData, onDataChange, actions } = this.props;
if (this.props.reducerData && !isEqual(reducerData, prevProps.reducerData)) {
onDataChange(this.props.reducerData);
}
if (data && !isEqual(data, prevProps.data)) {
actions.loadBoard(data);
onDataChange(data);
}
}
onDragStart = ({ payload }) => {
const { handleLaneDragStart } = this.props;
handleLaneDragStart(payload.id);
};
onLaneDrop = ({ removedIndex, addedIndex, payload }) => {
const { actions, handleLaneDragEnd } = this.props;
if (removedIndex !== addedIndex) {
actions.moveLane({ oldIndex: removedIndex, newIndex: addedIndex });
handleLaneDragEnd(removedIndex, addedIndex, payload);
}
};
getCardDetails = (laneId, cardIndex) => {
return this.props.reducerData.lanes.find((lane) => lane.id === laneId).cards[cardIndex];
};
getLaneDetails = (index) => {
return this.props.reducerData.lanes[index];
};
wireEventBus = () => {
const { actions, eventBusHandle } = this.props;
let eventBus = {
publish: (event) => {
switch (event.type) {
case "ADD_CARD":
return actions.addCard({ laneId: event.laneId, card: event.card });
case "UPDATE_CARD":
return actions.updateCard({ laneId: event.laneId, card: event.card });
case "REMOVE_CARD":
return actions.removeCard({ laneId: event.laneId, cardId: event.cardId });
case "REFRESH_BOARD":
return actions.loadBoard(event.data);
case "MOVE_CARD":
return actions.moveCardAcrossLanes({
fromLaneId: event.fromLaneId,
toLaneId: event.toLaneId,
cardId: event.cardId,
index: event.index
});
case "UPDATE_CARDS":
return actions.updateCards({ laneId: event.laneId, cards: event.cards });
case "UPDATE_CARD":
return actions.updateCard({ laneId: event.laneId, updatedCard: event.card });
case "UPDATE_LANES":
return actions.updateLanes(event.lanes);
case "UPDATE_LANE":
return actions.updateLane(event.lane);
default:
return;
}
}
};
eventBusHandle(eventBus);
};
// + add
hideEditableLane = () => {
this.setState({ addLaneMode: false });
};
showEditableLane = () => {
this.setState({ addLaneMode: true });
};
addNewLane = (params) => {
this.hideEditableLane();
this.props.actions.addLane(params);
this.props.onLaneAdd(params);
};
render() {
const {
id,
components,
reducerData,
draggable,
laneDraggable,
laneDragClass,
laneDropClass,
style,
onDataChange,
onCardAdd,
onCardUpdate,
onCardClick,
onBeforeCardDelete,
onCardDelete,
onLaneScroll,
onLaneClick,
onLaneAdd,
onLaneDelete,
onLaneUpdate,
editable,
canAddLanes,
laneStyle,
onCardMoveAcrossLanes,
t,
...otherProps
} = this.props;
const { addLaneMode } = this.state;
// Stick to whitelisting attributes to segregate board and lane props
const passthroughProps = pick(this.props, [
"onCardMoveAcrossLanes",
"onLaneScroll",
"onLaneDelete",
"onLaneUpdate",
"onCardClick",
"onBeforeCardDelete",
"onCardDelete",
"onCardAdd",
"onCardUpdate",
"onLaneClick",
"laneSortFunction",
"draggable",
"laneDraggable",
"cardDraggable",
"collapsibleLanes",
"canAddLanes",
"hideCardDeleteIcon",
"tagStyle",
"handleDragStart",
"handleDragEnd",
"cardDragClass",
"editLaneTitle",
"t"
]);
return (
<components.BoardWrapper style={style} {...otherProps} draggable={false}>
<PopoverWrapper>
<Container
orientation="horizontal"
onDragStart={this.onDragStart}
dragClass={laneDragClass}
dropClass={laneDropClass}
onDrop={this.onLaneDrop}
lockAxis="x"
getChildPayload={(index) => this.getLaneDetails(index)}
groupName={this.groupName}
>
{reducerData.lanes.map((lane, index) => {
const { id, droppable, ...otherProps } = lane;
const laneToRender = (
<Lane
key={id}
boardId={this.groupName}
components={components}
id={id}
getCardDetails={this.getCardDetails}
index={index}
droppable={droppable === undefined ? true : droppable}
style={laneStyle || lane.style || {}}
labelStyle={lane.labelStyle || {}}
cardStyle={this.props.cardStyle || lane.cardStyle}
editable={editable && !lane.disallowAddingCard}
{...otherProps}
{...passthroughProps}
/>
);
return draggable && laneDraggable ? <Draggable key={lane.id}>{laneToRender}</Draggable> : laneToRender;
})}
</Container>
</PopoverWrapper>
{canAddLanes && (
<Container orientation="horizontal">
{editable && !addLaneMode ? (
<components.NewLaneSection t={t} onClick={this.showEditableLane} />
) : (
addLaneMode && <components.NewLaneForm onCancel={this.hideEditableLane} onAdd={this.addNewLane} t={t} />
)}
</Container>
)}
</components.BoardWrapper>
);
}
}
BoardContainer.propTypes = {
id: PropTypes.string,
components: PropTypes.object,
actions: PropTypes.object,
data: PropTypes.object.isRequired,
reducerData: PropTypes.object,
onDataChange: PropTypes.func,
eventBusHandle: PropTypes.func,
onLaneScroll: PropTypes.func,
onCardClick: PropTypes.func,
onBeforeCardDelete: PropTypes.func,
onCardDelete: PropTypes.func,
onCardAdd: PropTypes.func,
onCardUpdate: PropTypes.func,
onLaneAdd: PropTypes.func,
onLaneDelete: PropTypes.func,
onLaneClick: PropTypes.func,
onLaneUpdate: PropTypes.func,
laneSortFunction: PropTypes.func,
draggable: PropTypes.bool,
collapsibleLanes: PropTypes.bool,
editable: PropTypes.bool,
canAddLanes: PropTypes.bool,
hideCardDeleteIcon: PropTypes.bool,
handleDragStart: PropTypes.func,
handleDragEnd: PropTypes.func,
handleLaneDragStart: PropTypes.func,
handleLaneDragEnd: PropTypes.func,
style: PropTypes.object,
tagStyle: PropTypes.object,
laneDraggable: PropTypes.bool,
cardDraggable: PropTypes.bool,
cardDragClass: PropTypes.string,
laneDragClass: PropTypes.string,
laneDropClass: PropTypes.string,
onCardMoveAcrossLanes: PropTypes.func.isRequired
};
BoardContainer.defaultProps = {
t: (v) => v,
onDataChange: () => {},
handleDragStart: () => {},
handleDragEnd: () => {},
handleLaneDragStart: () => {},
handleLaneDragEnd: () => {},
onCardUpdate: () => {},
onLaneAdd: () => {},
onLaneDelete: () => {},
onCardMoveAcrossLanes: () => {},
onLaneUpdate: () => {},
editable: false,
canAddLanes: false,
hideCardDeleteIcon: false,
draggable: false,
collapsibleLanes: false,
laneDraggable: true,
cardDraggable: true,
cardDragClass: "react_trello_dragClass",
laneDragClass: "react_trello_dragLaneClass",
laneDropClass: ""
};
const mapStateToProps = (state) => {
return state.trello.lanes ? { reducerData: state.trello } : {};
};
const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators({ ...actions }, dispatch)
});
export default connect(mapStateToProps, mapDispatchToProps)(BoardContainer);

View File

@@ -0,0 +1,328 @@
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);