Progress Commit

Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
Dave Richer
2024-05-17 16:29:46 -04:00
parent c3108a17f4
commit 55d729339f
9 changed files with 440 additions and 511 deletions

View File

@@ -55,7 +55,6 @@ export function ProductionBoardKanbanComponent({
const [isMoving, setIsMoving] = useState(false); const [isMoving, setIsMoving] = useState(false);
const [orientation, setOrientation] = useState("horizontal"); const [orientation, setOrientation] = useState("horizontal");
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => { useEffect(() => {
@@ -223,7 +222,7 @@ export function ProductionBoardKanbanComponent({
const components = { const components = {
Card: (cardProps) => ProductionBoardCard({ card: cardProps, technician, bodyshop, cardSettings }), Card: (cardProps) => ProductionBoardCard({ card: cardProps, technician, bodyshop, cardSettings }),
LaneHeader: cardSettings.stickyheader ? StickyHeader : NormalHeader LaneHeader: cardSettings.stickyheader && orientation === "horizontal" ? StickyHeader : NormalHeader
}; };
return ( return (
@@ -255,9 +254,8 @@ export function ProductionBoardKanbanComponent({
<StickyContainer> <StickyContainer>
<Board <Board
data={boardLanes} data={boardLanes}
draggable
handleDragEnd={handleDragEnd} handleDragEnd={handleDragEnd}
style={{ height: "100%", backgroundColor: "transparent" }} style={{ height: "100%", backgroundColor: "transparent", overflowY: "auto" }}
components={components} components={components}
orientation={orientation} orientation={orientation}
collapsibleLanes collapsibleLanes
@@ -267,9 +265,8 @@ export function ProductionBoardKanbanComponent({
<div> <div>
<Board <Board
data={boardLanes} data={boardLanes}
draggable
handleDragEnd={handleDragEnd} handleDragEnd={handleDragEnd}
style={{ backgroundColor: "transparent" }} style={{ backgroundColor: "transparent", overflowY: "auto" }}
components={components} components={components}
collapsibleLanes collapsibleLanes
orientation={orientation} orientation={orientation}

View File

@@ -7,8 +7,8 @@ const Board = ({ id, className, components, orientation, ...additionalProps }) =
const [storeId] = useState(id || v1()); const [storeId] = useState(id || v1());
const allClassNames = classNames("react-trello-board", className || ""); const allClassNames = classNames("react-trello-board", className || "");
const Styles = orientation === "horizontal" ? components.GlobalStyleHorizontal : components.GlobalStyleVertical; const Styles = orientation === "horizontal" ? components.GlobalStyleHorizontal : components.GlobalStyleVertical;
return ( return (
<> <>
<Styles /> <Styles />

View File

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

View File

@@ -1,4 +1,4 @@
import React, { Component } from "react"; import React, { useState, useEffect, useRef, useCallback, useMemo } from "react";
import classNames from "classnames"; import classNames from "classnames";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { bindActionCreators } from "redux"; import { bindActionCreators } from "redux";
@@ -12,140 +12,183 @@ import Draggable from "../dnd/Draggable.jsx";
import * as actions from "../../../redux/trello/trello.actions.js"; import * as actions from "../../../redux/trello/trello.actions.js";
class Lane extends Component { const defaultProps = {
state = { style: {},
loading: false, titleStyle: {},
currentPage: this.props.currentPage, labelStyle: {},
addCardMode: false, label: undefined,
collapsed: false, editable: false,
isDraggingOver: false onLaneUpdate: () => {},
}; onCardAdd: () => {},
onCardUpdate: () => {},
onCardDelete: () => {},
onBeforeCardDelete: () => {},
onLaneDelete: () => {},
onLaneClick: () => {},
onLaneScroll: () => {},
onCardClick: () => {},
onCardMoveAcrossLanes: () => {},
draggable: false,
laneDraggable: false,
cardDraggable: true,
collapsibleLanes: false,
droppable: true,
canAddLanes: false,
hideCardDeleteIcon: false,
components: {},
handleDragStart: () => {},
handleDragEnd: () => {},
orientation: "vertical"
};
get groupName() { const Lane = (internalProps) => {
const { boardId } = this.props; const props = useMemo(() => ({ ...defaultProps, ...internalProps }), [internalProps]);
return `TrelloBoard${boardId}Lane`; const [loading, setLoading] = useState(false);
} const [currentPage, setCurrentPage] = useState(props.currentPage);
const [addCardMode, setAddCardMode] = useState(false);
const [collapsed, setCollapsed] = useState(false);
const [isDraggingOver, setIsDraggingOver] = useState(false);
handleScroll = (evt) => { const laneRef = useRef(null);
const node = evt.target;
const elemScrollPosition = node.scrollHeight - node.scrollTop - node.clientHeight; useEffect(() => {
const { onLaneScroll } = this.props; if (!isEqual(props.cards, currentPage)) {
// 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 setCurrentPage(props.currentPage);
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 });
});
} }
}; }, [props.cards, props.currentPage, currentPage]);
sortCards(cards, sortFunction) { const handleScroll = useCallback(
(evt) => {
const node = evt.target;
const elemScrollPosition = node.scrollHeight - node.scrollTop - node.clientHeight;
const { onLaneScroll } = props;
if (elemScrollPosition < 1 && onLaneScroll && !loading) {
const nextPage = currentPage + 1;
setLoading(true);
onLaneScroll(nextPage, props.id).then((moreCards) => {
if ((moreCards || []).length > 0) {
props.actions.paginateLane({
laneId: props.id,
newCards: moreCards,
nextPage: nextPage
});
}
setLoading(false);
});
}
},
[currentPage, loading, props]
);
useEffect(() => {
const node = laneRef.current;
if (node) {
node.addEventListener("scroll", handleScroll);
}
return () => {
if (node) {
node.removeEventListener("scroll", handleScroll);
}
};
}, [handleScroll]);
const sortCards = (cards, sortFunction) => {
if (!cards) return []; if (!cards) return [];
if (!sortFunction) return cards; if (!sortFunction) return cards;
return cards.concat().sort(function (card1, card2) { return cards.concat().sort((card1, card2) => sortFunction(card1, card2));
return sortFunction(card1, card2);
});
}
laneDidMount = (node) => {
if (node) {
node.addEventListener("scroll", this.handleScroll);
}
}; };
static getDerivedStateFromProps(nextProps, prevState) { const removeCard = (cardId) => {
if (!isEqual(prevState.cards, nextProps.cards)) { if (props.onBeforeCardDelete && typeof props.onBeforeCardDelete === "function") {
return { props.onBeforeCardDelete(() => {
currentPage: nextProps.currentPage props.onCardDelete && props.onCardDelete(cardId, props.id);
}; props.actions.removeCard({ laneId: props.id, cardId: cardId });
}
// Return null if the state hasn't changed
return null;
}
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 { } else {
this.props.onCardDelete && this.props.onCardDelete(cardId, this.props.id); props.onCardDelete && props.onCardDelete(cardId, props.id);
this.props.actions.removeCard({ laneId: this.props.id, cardId: cardId }); props.actions.removeCard({ laneId: props.id, cardId: cardId });
} }
}; };
handleCardClick = (e, card) => { const handleCardClick = (e, card) => {
const { onCardClick } = this.props; const { onCardClick } = props;
onCardClick && onCardClick(card.id, card.metadata, card.laneId); onCardClick && onCardClick(card.id, card.metadata, card.laneId);
e.stopPropagation(); e.stopPropagation();
}; };
showEditableCard = () => { const showEditableCard = () => {
this.setState({ addCardMode: true }); setAddCardMode(true);
}; };
hideEditableCard = () => { const hideEditableCard = () => {
this.setState({ addCardMode: false }); setAddCardMode(false);
}; };
addNewCard = (params) => { const addNewCard = (params) => {
const laneId = this.props.id; const laneId = props.id;
const id = v1(); const id = v1();
this.hideEditableCard(); hideEditableCard();
let card = { id, ...params }; let card = { id, ...params };
this.props.actions.addCard({ laneId, card }); props.actions.addCard({ laneId, card });
this.props.onCardAdd(card, laneId); props.onCardAdd(card, laneId);
}; };
onDragStart = ({ payload }) => { const onDragStart = ({ payload }) => {
const { handleDragStart } = this.props; const { handleDragStart } = props;
handleDragStart && handleDragStart(payload.id, payload.laneId); handleDragStart && handleDragStart(payload.id, payload.laneId);
}; };
shouldAcceptDrop = (sourceContainerOptions) => { const shouldAcceptDrop = (sourceContainerOptions) => {
return this.props.droppable && sourceContainerOptions.groupName === this.groupName; return props.droppable && sourceContainerOptions.groupName === groupName;
}; };
onDragEnd = (laneId, result) => { const onDragEnd = (laneId, result) => {
const { handleDragEnd } = this.props; const { handleDragEnd } = props;
const { addedIndex, payload } = result; const { addedIndex, payload } = result;
if (this.state.isDraggingOver) { if (isDraggingOver) {
this.setState({ isDraggingOver: false }); setIsDraggingOver(false);
} }
if (addedIndex != null) { if (addedIndex != null) {
const newCard = { ...cloneDeep(payload), laneId }; const newCard = { ...cloneDeep(payload), laneId };
const response = handleDragEnd ? handleDragEnd(payload.id, payload.laneId, laneId, addedIndex, newCard) : true; const response = handleDragEnd ? handleDragEnd(payload.id, payload.laneId, laneId, addedIndex, newCard) : true;
if (response === undefined || !!response) { if (response === undefined || !!response) {
this.props.actions.moveCardAcrossLanes({ props.actions.moveCardAcrossLanes({
fromLaneId: payload.laneId, fromLaneId: payload.laneId,
toLaneId: laneId, toLaneId: laneId,
cardId: payload.id, cardId: payload.id,
index: addedIndex index: addedIndex
}); });
this.props.onCardMoveAcrossLanes(payload.laneId, laneId, payload.id, addedIndex); props.onCardMoveAcrossLanes(payload.laneId, laneId, payload.id, addedIndex);
} }
return response; return response;
} }
}; };
updateCard = (updatedCard) => { const updateCard = (updatedCard) => {
this.props.actions.updateCard({ laneId: this.props.id, card: updatedCard }); props.actions.updateCard({ laneId: props.id, card: updatedCard });
this.props.onCardUpdate(this.props.id, updatedCard); props.onCardUpdate(props.id, updatedCard);
}; };
renderDragContainer = (isDraggingOver) => { const removeLane = () => {
const { id } = props;
props.actions.removeLane({ laneId: id });
props.onLaneDelete(id);
};
const updateTitle = (value) => {
props.actions.updateLane({ id: props.id, title: value });
props.onLaneUpdate(props.id, { title: value });
};
const toggleLaneCollapsed = () => {
props.collapsibleLanes && setCollapsed(!collapsed);
};
const groupName = `TrelloBoard${props.boardId}Lane`;
const renderDragContainer = (isDraggingOver) => {
const { const {
id, id,
cards, cards,
@@ -159,13 +202,12 @@ class Lane extends Component {
cardStyle, cardStyle,
components, components,
orientation orientation
} = this.props; } = props;
const { addCardMode, collapsed } = this.state;
const stableCards = collapsed ? [] : cards; const stableCards = collapsed ? [] : cards;
const cardList = this.sortCards(stableCards, laneSortFunction).map((card, idx) => { const cardList = sortCards(stableCards, laneSortFunction).map((card, idx) => {
const onDeleteCard = () => this.removeCard(card.id); const onDeleteCard = () => removeCard(card.id);
const cardToRender = ( const cardToRender = (
<components.Card <components.Card
key={card.id} key={card.id}
@@ -173,8 +215,8 @@ class Lane extends Component {
style={card.style || cardStyle} style={card.style || cardStyle}
className="react-trello-card" className="react-trello-card"
onDelete={onDeleteCard} onDelete={onDeleteCard}
onClick={(e) => this.handleCardClick(e, card)} onClick={(e) => handleCardClick(e, card)}
onChange={(updatedCard) => this.updateCard(updatedCard)} onChange={(updatedCard) => updateCard(updatedCard)}
showDeleteButton={!hideCardDeleteIcon} showDeleteButton={!hideCardDeleteIcon}
tagStyle={tagStyle} tagStyle={tagStyle}
cardDraggable={cardDraggable} cardDraggable={cardDraggable}
@@ -190,93 +232,59 @@ class Lane extends Component {
}); });
return ( return (
<components.ScrollableLane ref={this.laneDidMount} isDraggingOver={isDraggingOver}> <components.ScrollableLane ref={laneRef} isDraggingOver={isDraggingOver}>
<Container <Container
orientation={orientation === "horizontal" ? "vertical" : "horizontal"} orientation={orientation === "horizontal" ? "vertical" : "horizontal"}
groupName={this.groupName} groupName={groupName}
dragClass={cardDragClass} dragClass={cardDragClass}
dropClass={cardDropClass} dropClass={cardDropClass}
onDragStart={this.onDragStart} onDragStart={onDragStart}
onDrop={(e) => this.onDragEnd(id, e)} onDrop={(e) => onDragEnd(id, e)}
onDragEnter={() => this.setState({ isDraggingOver: true })} onDragEnter={() => setIsDraggingOver(true)}
onDragLeave={() => this.setState({ isDraggingOver: false })} onDragLeave={() => setIsDraggingOver(false)}
shouldAcceptDrop={this.shouldAcceptDrop} shouldAcceptDrop={shouldAcceptDrop}
getChildPayload={(index) => this.props.getCardDetails(id, index)} getChildPayload={(index) => props.getCardDetails(id, index)}
> >
{cardList} {cardList}
</Container> </Container>
{editable && !addCardMode && <components.AddCardLink onClick={this.showEditableCard} laneId={id} />} {editable && !addCardMode && <components.AddCardLink onClick={showEditableCard} laneId={id} />}
{addCardMode && <components.NewCardForm onCancel={this.hideEditableCard} laneId={id} onAdd={this.addNewCard} />} {addCardMode && <components.NewCardForm onCancel={hideEditableCard} laneId={id} onAdd={addNewCard} />}
</components.ScrollableLane> </components.ScrollableLane>
); );
}; };
removeLane = () => { const renderHeader = (pickedProps) => {
const { id } = this.props; const { components } = 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 ( return (
<components.LaneHeader <components.LaneHeader
{...pickedProps} {...pickedProps}
onDelete={this.removeLane} onDelete={removeLane}
onDoubleClick={this.toggleLaneCollapsed} onDoubleClick={toggleLaneCollapsed}
updateTitle={this.updateTitle} updateTitle={updateTitle}
/> />
); );
}; };
toggleLaneCollapsed = () => { const { id, cards, collapsibleLanes, components, onLaneClick, orientation, ...otherProps } = props;
this.props.collapsibleLanes && this.setState((state) => ({ collapsed: !state.collapsed })); const allClassNames = classNames("react-trello-lane", collapsed ? "lane-collapsed" : "", props.className || "");
}; const showFooter = collapsibleLanes && cards.length > 0;
render() { // Removed the ...otherProps spread from the components.Section
const { loading, isDraggingOver, collapsed } = this.state; return (
const { <components.Section
id, key={id}
cards, onClick={() => onLaneClick && onLaneClick(id)}
collapsibleLanes, draggable={false}
components, className={allClassNames}
onLaneClick, orientation={orientation}
onLaneScroll, >
onCardClick, {renderHeader({ id, cards, ...otherProps })}
onCardAdd, {renderDragContainer(isDraggingOver)}
onBeforeCardDelete, {loading && <components.Loader />}
onCardDelete, {showFooter && <components.LaneFooter onClick={toggleLaneCollapsed} collapsed={collapsed} />}
onLaneDelete, </components.Section>
onLaneUpdate, );
onCardUpdate, };
onCardMoveAcrossLanes,
orientation,
...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}
orientation={orientation}
>
{this.renderHeader({ id, cards, ...otherProps })}
{this.renderDragContainer(isDraggingOver)}
{loading && <components.Loader />}
{showFooter && <components.LaneFooter onClick={this.toggleLaneCollapsed} collapsed={collapsed} />}
</components.Section>
);
}
}
Lane.propTypes = { Lane.propTypes = {
actions: PropTypes.object, actions: PropTypes.object,
@@ -320,35 +328,6 @@ Lane.propTypes = {
orientation: PropTypes.string orientation: PropTypes.string
}; };
Lane.defaultProps = {
style: {},
titleStyle: {},
labelStyle: {},
label: undefined,
editable: false,
onLaneUpdate: () => {},
onCardAdd: () => {},
onCardUpdate: () => {},
onCardDelete: () => {},
onBeforeCardDelete: () => {},
onLaneDelete: () => {},
onLaneClick: () => {},
onLaneScroll: () => {},
onCardClick: () => {},
onCardMoveAcrossLanes: () => {},
draggable: false,
laneDraggable: false,
cardDraggable: true,
collapsibleLanes: false,
droppable: true,
canAddLanes: false,
hideCardDeleteIcon: false,
components: {},
handleDragStart: () => {},
handleDragEnd: () => {},
orientation: "vertical"
};
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actions, dispatch) actions: bindActionCreators(actions, dispatch)
}); });

View File

@@ -51,52 +51,27 @@ class Container extends Component {
getContainerOptions() { getContainerOptions() {
const functionProps = {}; const functionProps = {};
const propKeys = [
"onDragStart",
"onDragEnd",
"onDrop",
"getChildPayload",
"shouldAnimateDrop",
"shouldAcceptDrop",
"onDragEnter",
"onDragLeave",
"render",
"onDropReady",
"getGhostParent"
];
if (this.props.onDragStart) { propKeys.forEach((key) => {
functionProps.onDragStart = (...p) => this.props.onDragStart(...p); if (this.props[key]) {
} functionProps[key] = (...p) => this.props[key](...p);
}
});
if (this.props.onDragEnd) { return { ...this.props, ...functionProps };
functionProps.onDragEnd = (...p) => this.props.onDragEnd(...p);
}
if (this.props.onDrop) {
functionProps.onDrop = (...p) => this.props.onDrop(...p);
}
if (this.props.getChildPayload) {
functionProps.getChildPayload = (...p) => this.props.getChildPayload(...p);
}
if (this.props.shouldAnimateDrop) {
functionProps.shouldAnimateDrop = (...p) => this.props.shouldAnimateDrop(...p);
}
if (this.props.shouldAcceptDrop) {
functionProps.shouldAcceptDrop = (...p) => this.props.shouldAcceptDrop(...p);
}
if (this.props.onDragEnter) {
functionProps.onDragEnter = (...p) => this.props.onDragEnter(...p);
}
if (this.props.onDragLeave) {
functionProps.onDragLeave = (...p) => this.props.onDragLeave(...p);
}
if (this.props.render) {
functionProps.render = (...p) => this.props.render(...p);
}
if (this.props.onDropReady) {
functionProps.onDropReady = (...p) => this.props.onDropReady(...p);
}
if (this.props.getGhostParent) {
functionProps.getGhostParent = (...p) => this.props.getGhostParent(...p);
}
return Object.assign({}, this.props, functionProps);
} }
} }

View File

@@ -1,26 +1,35 @@
import React, {Component} from 'react' import React, { Component } from "react";
import PropTypes from 'prop-types' import PropTypes from "prop-types";
import {constants} from 'kuika-smooth-dnd' import { constants } from "kuika-smooth-dnd";
const {wrapperClass} = constants const { wrapperClass } = constants;
class Draggable extends Component { class Draggable extends Component {
render() { render() {
if (this.props.render) { const { render, className, children, ...restProps } = this.props;
return React.cloneElement(this.props.render(), {className: wrapperClass})
}
const clsName = `${this.props.className ? this.props.className + ' ' : ''}` try {
return ( if (render) {
<div {...this.props} className={`${clsName}${wrapperClass}`}> return React.cloneElement(render(), { className: wrapperClass });
{this.props.children} }
</div>
) const clsName = className ? `${className} ` : "";
return (
<div {...restProps} className={`${clsName}${wrapperClass}`}>
{children}
</div>
);
} catch (error) {
console.error("Error rendering Draggable component:", error);
return null; // Return null if an error occurs to prevent crashing
}
} }
} }
Draggable.propTypes = { Draggable.propTypes = {
render: PropTypes.func render: PropTypes.func,
} className: PropTypes.string,
children: PropTypes.node
};
export default Draggable export default Draggable;

View File

@@ -1,5 +1,9 @@
import update from "immutability-helper"; import update from "immutability-helper";
const updateLanes = (state, lanes) => update(state, { lanes: { $set: lanes } });
const updateLaneCards = (lane, cards) => update(lane, { cards: { $set: cards } });
const LaneHelper = { const LaneHelper = {
initialiseLanes: (state, { lanes }) => { initialiseLanes: (state, { lanes }) => {
const newLanes = lanes.map((lane) => { const newLanes = lanes.map((lane) => {
@@ -7,13 +11,13 @@ const LaneHelper = {
lane.cards && lane.cards.forEach((c) => (c.laneId = lane.id)); lane.cards && lane.cards.forEach((c) => (c.laneId = lane.id));
return lane; return lane;
}); });
return update(state, { lanes: { $set: newLanes } }); return updateLanes(state, newLanes);
}, },
paginateLane: (state, { laneId, newCards, nextPage }) => { paginateLane: (state, { laneId, newCards, nextPage }) => {
const updatedLanes = LaneHelper.appendCardsToLane(state, { laneId: laneId, newCards: newCards }); const updatedLanes = LaneHelper.appendCardsToLane(state, { laneId: laneId, newCards: newCards });
updatedLanes.find((lane) => lane.id === laneId).currentPage = nextPage; updatedLanes.find((lane) => lane.id === laneId).currentPage = nextPage;
return update(state, { lanes: { $set: updatedLanes } }); return updateLanes(state, updatedLanes);
}, },
appendCardsToLane: (state, { laneId, newCards, index }) => { appendCardsToLane: (state, { laneId, newCards, index }) => {
@@ -23,12 +27,11 @@ const LaneHelper = {
.filter((c) => lane.cards.find((card) => card.id === c.id) == null); .filter((c) => lane.cards.find((card) => card.id === c.id) == null);
return state.lanes.map((lane) => { return state.lanes.map((lane) => {
if (lane.id === laneId) { if (lane.id === laneId) {
if (index !== undefined) { const cardsToUpdate =
return update(lane, { cards: { $splice: [[index, 0, ...newCards]] } }); index !== undefined
} else { ? [...lane.cards.slice(0, index), ...newCards, ...lane.cards.slice(index)]
const cardsToUpdate = [...lane.cards, ...newCards]; : [...lane.cards, ...newCards];
return update(lane, { cards: { $set: cardsToUpdate } }); return updateLaneCards(lane, cardsToUpdate);
}
} else { } else {
return lane; return lane;
} }
@@ -37,35 +40,29 @@ const LaneHelper = {
appendCardToLane: (state, { laneId, card, index }) => { appendCardToLane: (state, { laneId, card, index }) => {
const newLanes = LaneHelper.appendCardsToLane(state, { laneId: laneId, newCards: [card], index }); const newLanes = LaneHelper.appendCardsToLane(state, { laneId: laneId, newCards: [card], index });
return update(state, { lanes: { $set: newLanes } }); return updateLanes(state, newLanes);
}, },
addLane: (state, lane) => { addLane: (state, lane) => {
const newLane = { cards: [], ...lane }; const newLane = { cards: [], ...lane };
return update(state, { lanes: { $push: [newLane] } }); return updateLanes(state, [...state.lanes, newLane]);
}, },
updateLane: (state, updatedLane) => { updateLane: (state, updatedLane) => {
const newLanes = state.lanes.map((lane) => { const newLanes = state.lanes.map((lane) => (updatedLane.id === lane.id ? { ...lane, ...updatedLane } : lane));
if (updatedLane.id === lane.id) { return updateLanes(state, newLanes);
return { ...lane, ...updatedLane };
} else {
return lane;
}
});
return update(state, { lanes: { $set: newLanes } });
}, },
removeCardFromLane: (state, { laneId, cardId }) => { removeCardFromLane: (state, { laneId, cardId }) => {
const lanes = state.lanes.map((lane) => { const lanes = state.lanes.map((lane) => {
if (lane.id === laneId) { if (lane.id === laneId) {
let newCards = lane.cards.filter((card) => card.id !== cardId); const newCards = lane.cards.filter((card) => card.id !== cardId);
return update(lane, { cards: { $set: newCards } }); return updateLaneCards(lane, newCards);
} else { } else {
return lane; return lane;
} }
}); });
return update(state, { lanes: { $set: lanes } }); return updateLanes(state, lanes);
}, },
moveCardAcrossLanes: (state, { fromLaneId, toLaneId, cardId, index }) => { moveCardAcrossLanes: (state, { fromLaneId, toLaneId, cardId, index }) => {
@@ -74,50 +71,36 @@ const LaneHelper = {
if (lane.id === fromLaneId) { if (lane.id === fromLaneId) {
cardToMove = lane.cards.find((card) => card.id === cardId); cardToMove = lane.cards.find((card) => card.id === cardId);
const newCards = lane.cards.filter((card) => card.id !== cardId); const newCards = lane.cards.filter((card) => card.id !== cardId);
return update(lane, { cards: { $set: newCards } }); return updateLaneCards(lane, newCards);
} else { } else {
return lane; return lane;
} }
}); });
const updatedState = update(state, { lanes: { $set: interimLanes } }); return LaneHelper.appendCardToLane(
return LaneHelper.appendCardToLane(updatedState, { { ...state, lanes: interimLanes },
laneId: toLaneId, {
card: cardToMove, laneId: toLaneId,
index: index card: cardToMove,
}); index: index
}
);
}, },
updateCardsForLane: (state, { laneId, cards }) => { updateCardsForLane: (state, { laneId, cards }) => {
const lanes = state.lanes.map((lane) => { const lanes = state.lanes.map((lane) => (lane.id === laneId ? updateLaneCards(lane, cards) : lane));
if (lane.id === laneId) { return updateLanes(state, lanes);
return update(lane, { cards: { $set: cards } });
} else {
return lane;
}
});
return update(state, { lanes: { $set: lanes } });
}, },
updateCardForLane: (state, { laneId, card: updatedCard }) => { updateCardForLane: (state, { laneId, card: updatedCard }) => {
const lanes = state.lanes.map((lane) => { const lanes = state.lanes.map((lane) => {
if (lane.id === laneId) { if (lane.id === laneId) {
const cards = lane.cards.map((card) => { const cards = lane.cards.map((card) => (card.id === updatedCard.id ? { ...card, ...updatedCard } : card));
if (card.id === updatedCard.id) { return updateLaneCards(lane, cards);
return { ...card, ...updatedCard };
} else {
return card;
}
});
return update(lane, { cards: { $set: cards } });
} else { } else {
return lane; return lane;
} }
}); });
return update(state, { lanes: { $set: lanes } }); return updateLanes(state, lanes);
},
updateLanes: (state, lanes) => {
return { ...state, ...{ lanes: lanes } };
}, },
moveLane: (state, { oldIndex, newIndex }) => { moveLane: (state, { oldIndex, newIndex }) => {
@@ -128,7 +111,7 @@ const LaneHelper = {
removeLane: (state, { laneId }) => { removeLane: (state, { laneId }) => {
const updatedLanes = state.lanes.filter((lane) => lane.id !== laneId); const updatedLanes = state.lanes.filter((lane) => lane.id !== laneId);
return update(state, { lanes: { $set: updatedLanes } }); return updateLanes(state, updatedLanes);
} }
}; };

View File

@@ -60,7 +60,6 @@ export const GlobalStyleHorizontal = createGlobalStyle`
width: 32px; width: 32px;
} }
`; `;
export const GlobalStyleVertical = createGlobalStyle` export const GlobalStyleVertical = createGlobalStyle`
.comPlainTextContentEditable { .comPlainTextContentEditable {
-webkit-user-modify: read-write-plaintext-only; -webkit-user-modify: read-write-plaintext-only;
@@ -101,7 +100,8 @@ export const GlobalStyleVertical = createGlobalStyle`
} }
.smooth-dnd-container { .smooth-dnd-container {
//min-height: 100px; // Not needed, just for extra landing space // TODO ? This is the question. We need the same drag-zone we get in horizontal mode
min-height: 50px; // Not needed, just for extra landing space
} }
.react-trello-lane { .react-trello-lane {
@@ -186,9 +186,9 @@ export const Header = styled.header`
export const Section = styled.section` export const Section = styled.section`
background-color: #e3e3e3; background-color: #e3e3e3;
border-radius: 3px; border-radius: 3px;
margin: 5px 5px; margin: 2px 2px;
position: relative; position: relative;
padding: 10px; padding: 5px;
${getSectionStyles}; ${getSectionStyles};
flex-direction: column; flex-direction: column;
`; `;

View File

@@ -1,10 +1,13 @@
import React from "react"; import React from "react";
import { DelButton, DeleteWrapper } from "../styles/Elements"; import { DeleteWrapper } from "../styles/Elements";
import { Button } from "antd";
const DeleteButton = (props) => { const DeleteButton = (props) => {
return ( return (
<DeleteWrapper {...props}> <DeleteWrapper {...props}>
<DelButton>&#10006;</DelButton> <Button type="primary" danger>
Delete
</Button>
</DeleteWrapper> </DeleteWrapper>
); );
}; };