- Clear stage prior to implementing replacement for collapsed lanes (with virtual lists)

Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
Dave Richer
2024-06-28 15:08:26 -04:00
parent 8207a52b6b
commit 2f493c63f8
5 changed files with 313 additions and 325 deletions

View File

@@ -6,7 +6,6 @@ import { PageHeader } from "@ant-design/pro-layout";
import React, { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Sticky, StickyContainer } from "react-sticky";
import { createStructuredSelector } from "reselect";
import styled from "styled-components";
import { logImEXEvent } from "../../firebase/firebase.utils";
@@ -64,7 +63,7 @@ export function ProductionBoardKanbanComponent({
}, [associationSettings]);
useEffect(() => {
const boardData = createBoardData(
const boardData = createFakeBoardData(
[...bodyshop.md_ro_statuses.production_statuses, ...(bodyshop.md_ro_statuses.additional_board_statuses || [])],
data,
filter
@@ -114,89 +113,90 @@ export function ProductionBoardKanbanComponent({
};
// TODO, refine and use action
const onDragEnd = async ({ draggableId, type, source, reason, mode, destination, combine }) => {
//cardId, sourceLaneId, targetLaneId, position, cardDetails
logImEXEvent("kanban_drag_end");
// Early Gate
if (!type || type !== "lane" || !source || !destination) return;
setIsMoving(true);
const sameColumnTransfer = source.droppableId === destination.droppableId;
const targetLane = boardLanes.lanes[Number.parseInt(destination.droppableId)];
const sourceLane = boardLanes.lanes[Number.parseInt(source.droppableId)];
const sourceCard = getCardByID(boardLanes, draggableId);
const movedCardWillBeFirst = destination.index === 0;
const movedCardWillBeLast = destination.index > targetLane.cards.length - 1;
const movedCardIsFirstNewCard = movedCardWillBeFirst && movedCardWillBeLast;
const lastCardInTargetLane = targetLane.cards[targetLane.cards.length - 1];
const oldChildCard = sourceLane.cards[destination.index + 1];
const onDragEnd = async (...args) => {
console.dir(args);
// //cardId, sourceLaneId, targetLaneId, position, cardDetails
// logImEXEvent("kanban_drag_end");
//
const newChildCard = movedCardWillBeLast
? null
: targetLane.cards[
sameColumnTransfer
? destination.index - destination.index > 0
? destination.index
: destination.index + 1
: destination.index
];
const oldChildCardNewParent = oldChildCard ? sourceCard.metadata.kanbanparent : null;
let movedCardNewKanbanParent;
if (movedCardWillBeFirst) {
movedCardNewKanbanParent = "-1";
} else if (movedCardWillBeLast) {
movedCardNewKanbanParent = lastCardInTargetLane.id;
} else if (!!newChildCard) {
movedCardNewKanbanParent = newChildCard.metadata.kanbanparent;
} else {
console.log("==> !!!!!!Couldn't find a parent.!!!! <==");
}
const newChildCardNewParent = newChildCard ? draggableId : null;
try {
const update = await client.mutate({
mutation: generate_UPDATE_JOB_KANBAN(
oldChildCard ? oldChildCard.id : null,
oldChildCardNewParent,
draggableId,
movedCardNewKanbanParent,
targetLane.id,
newChildCard ? newChildCard.id : null,
newChildCardNewParent
)
});
insertAuditTrail({
jobid: draggableId,
operation: AuditTrailMapping.jobstatuschange(targetLane.id),
type: "jobstatuschange"
});
if (update.errors) {
notification["error"]({
message: t("production.errors.boardupdate", {
message: JSON.stringify(update.errors)
})
});
}
} catch (error) {
notification["error"]({
message: t("production.errors.boardupdate", {
message: error.message
})
});
} finally {
setIsMoving(false);
}
// // Early Gate
// if (!type || type !== "lane" || !source || !destination) return;
//
// setIsMoving(true);
//
// const sameColumnTransfer = source.droppableId === destination.droppableId;
// const targetLane = boardLanes.lanes[Number.parseInt(destination.droppableId)];
// const sourceLane = boardLanes.lanes[Number.parseInt(source.droppableId)];
// const sourceCard = getCardByID(boardLanes, draggableId);
//
// const movedCardWillBeFirst = destination.index === 0;
// const movedCardWillBeLast = destination.index > targetLane.cards.length - 1;
// const movedCardIsFirstNewCard = movedCardWillBeFirst && movedCardWillBeLast;
//
// const lastCardInTargetLane = targetLane.cards[targetLane.cards.length - 1];
//
// const oldChildCard = sourceLane.cards[destination.index + 1];
//
// //
// const newChildCard = movedCardWillBeLast
// ? null
// : targetLane.cards[
// sameColumnTransfer
// ? destination.index - destination.index > 0
// ? destination.index
// : destination.index + 1
// : destination.index
// ];
//
// const oldChildCardNewParent = oldChildCard ? sourceCard.metadata.kanbanparent : null;
//
// let movedCardNewKanbanParent;
// if (movedCardWillBeFirst) {
// movedCardNewKanbanParent = "-1";
// } else if (movedCardWillBeLast) {
// movedCardNewKanbanParent = lastCardInTargetLane.id;
// } else if (!!newChildCard) {
// movedCardNewKanbanParent = newChildCard.metadata.kanbanparent;
// } else {
// console.log("==> !!!!!!Couldn't find a parent.!!!! <==");
// }
// const newChildCardNewParent = newChildCard ? draggableId : null;
// try {
// const update = await client.mutate({
// mutation: generate_UPDATE_JOB_KANBAN(
// oldChildCard ? oldChildCard.id : null,
// oldChildCardNewParent,
// draggableId,
// movedCardNewKanbanParent,
// targetLane.id,
// newChildCard ? newChildCard.id : null,
// newChildCardNewParent
// )
// });
//
// insertAuditTrail({
// jobid: draggableId,
// operation: AuditTrailMapping.jobstatuschange(targetLane.id),
// type: "jobstatuschange"
// });
//
// if (update.errors) {
// notification["error"]({
// message: t("production.errors.boardupdate", {
// message: JSON.stringify(update.errors)
// })
// });
// }
// } catch (error) {
// notification["error"]({
// message: t("production.errors.boardupdate", {
// message: error.message
// })
// });
// } finally {
// setIsMoving(false);
// }
// };
};
const totalHrs = useMemo(
() =>
data
@@ -219,45 +219,7 @@ export function ProductionBoardKanbanComponent({
[data]
);
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
const standardSizes = {
xs: "250",
sm: "250",
md: "250",
lg: "250",
xl: "250",
xxl: "250"
};
const compactSizes = {
xs: "150",
sm: "150",
md: "150",
lg: "150",
xl: "155",
xxl: "155"
};
const width = selectedBreakpoint
? associationSettings && associationSettings.kanban_settings && associationSettings.kanban_settings.compact
? compactSizes[selectedBreakpoint[0]]
: standardSizes[selectedBreakpoint[0]]
: "250";
const StickyHeader = ({ title }) => (
<Sticky>
{({ style }) => (
<div className="react-trello-column-header" style={{ ...style, zIndex: "99", backgroundColor: "#e3e3e3" }}>
<UnorderedListOutlined style={{ marginRight: "5px" }} /> {title}
</div>
)}
</Sticky>
);
const NormalHeader = ({ title }) => (
const Header = ({ title }) => (
<div className="react-trello-column-header" style={{ backgroundColor: "#e3e3e3" }}>
<UnorderedListOutlined style={{ marginRight: "5px" }} /> {title}
</div>
@@ -285,7 +247,7 @@ export function ProductionBoardKanbanComponent({
const components = {
Card: (cardProps) => ProductionBoardCard({ card: cardProps, technician, bodyshop, cardSettings }),
LaneHeader: cardSettings.stickyheader && orientation === "horizontal" ? StickyHeader : NormalHeader
LaneHeader: Header
};
if (loading) {
@@ -293,7 +255,7 @@ export function ProductionBoardKanbanComponent({
}
return (
<Container width={width}>
<div>
<IndefiniteLoading loading={isMoving} />
<PageHeader
title={
@@ -316,41 +278,15 @@ export function ProductionBoardKanbanComponent({
/>
{cardSettings.cardcolor && <CardColorLegend cardSettings={cardSettings} bodyshop={bodyshop} />}
<ProductionListDetailComponent jobs={data} />
{cardSettings.stickyheader ? (
<StickyContainer>
<Board
data={boardLanes}
onDragEnd={onDragEnd}
style={{ height: "100%", backgroundColor: "transparent", overflowY: "auto" }}
components={components}
orientation={orientation}
collapsibleLanes
/>
</StickyContainer>
) : (
<div>
<Board
data={boardLanes}
onDragEnd={onDragEnd}
style={{ backgroundColor: "transparent", overflowY: "auto" }}
components={components}
collapsibleLanes
orientation={orientation}
/>
</div>
)}
</Container>
<Board
data={boardLanes}
onDragEnd={onDragEnd}
components={components}
collapsibleLanes
orientation={orientation}
/>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(ProductionBoardKanbanComponent);
const Container = styled.div`
.react-trello-card-skeleton,
.react-trello-card,
.react-trello-card-adder-form {
box-sizing: border-box;
max-width: ${(props) => props.width}px;
min-width: ${(props) => props.width}px;
}
`;

View File

@@ -2,12 +2,12 @@
padding: 5px;
}
.react-trello-card {
border-radius: 3px;
background-color: #fff;
padding: 4px;
margin-bottom: 7px;
}
//.react-trello-card {
// border-radius: 3px;
// background-color: #fff;
// padding: 4px;
// margin-bottom: 7px;
//}
// .react-trello-card-skeleton,
// .react-trello-card,
@@ -33,12 +33,12 @@
justify-content: space-between;
}
.react-trello-column {
padding: 10px;
border-radius: 2px;
background-color: #eee;
margin: 5px;
}
//.react-trello-column {
// padding: 10px;
// border-radius: 2px;
// background-color: #eee;
// margin: 5px;
//}
.react-trello-column input:focus {
outline: none;
@@ -84,6 +84,10 @@
width: 100%;
padding: 0px;
}
.height-preserving-container:empty {
min-height: calc(var(--child-height));
box-sizing: border-box;
}
.react-trello-card-adder-form__title:focus {
outline: none;

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import React, { forwardRef, useCallback, useEffect, useRef, useState } from "react";
import classNames from "classnames";
import PropTypes from "prop-types";
import { bindActionCreators } from "redux";
@@ -7,55 +7,32 @@ import isEqual from "lodash/isEqual";
import { v1 } from "uuid";
import * as actions from "../../../redux/trello/trello.actions.js";
import { Droppable, Draggable } from "../dnd/lib/index.js";
import { Draggable, Droppable } from "../dnd/lib";
import { Virtuoso, VirtuosoGrid } from "react-virtuoso";
function HeightPreservingItem({ children, ...props }) {
const [size, setSize] = useState(0);
const { "data-known-size": knownSize = 0 } = props;
useEffect(() => {
if (knownSize !== 0) {
setSize(knownSize);
}
}, [knownSize]);
return (
<div
{...props}
className="height-preserving-container"
style={{
"--child-height": `${size}px`
}}
>
{children}
</div>
);
}
/**
* Lane is a React component that represents a lane in a Trello-like board.
* It uses Redux for state management and provides a variety of props to customize its behavior.
*
* @component
* @param {Object} props - Component props
* @param {Object} props.actions - Redux actions
* @param {string} props.id - The unique identifier for the lane
* @param {string} props.boardId - The unique identifier for the board
* @param {string} props.title - The title of the lane
* @param {number} props.index - The index of the lane
* @param {Function} props.laneSortFunction - Function to sort the cards in the lane
* @param {Object} props.style - The CSS styles to apply to the lane
* @param {Object} props.cardStyle - The CSS styles to apply to the cards
* @param {Object} props.tagStyle - The CSS styles to apply to the tags
* @param {Object} props.titleStyle - The CSS styles to apply to the title
* @param {Object} props.labelStyle - The CSS styles to apply to the label
* @param {Array} props.cards - The cards in the lane
* @param {string} props.label - The label of the lane
* @param {boolean} props.draggable - Whether the lane is draggable
* @param {boolean} props.collapsibleLanes - Whether the lanes are collapsible
* @param {boolean} props.droppable - Whether the lane is droppable
* @param {Function} props.onCardClick - Callback function when a card is clicked
* @param {Function} props.onBeforeCardDelete - Callback function before a card is deleted
* @param {Function} props.onCardDelete - Callback function when a card is deleted
* @param {Function} props.onCardAdd - Callback function when a card is added
* @param {Function} props.onCardUpdate - Callback function when a card is updated
* @param {Function} props.onLaneDelete - Callback function when a lane is deleted
* @param {Function} props.onLaneUpdate - Callback function when a lane is updated
* @param {Function} props.onLaneClick - Callback function when a lane is clicked
* @param {Function} props.onLaneScroll - Callback function when a lane is scrolled
* @param {boolean} props.editable - Whether the lane is editable
* @param {boolean} props.cardDraggable - Whether the cards are draggable
* @param {string} props.cardDragClass - The CSS class to apply when a card is being dragged
* @param {string} props.cardDropClass - The CSS class to apply when a card is dropped
* @param {boolean} props.canAddLanes - Whether lanes can be added to the board
* @param {boolean} props.hideCardDeleteIcon - Whether to hide the card delete icon
* @param {Object} props.components - Custom components to use in the lane
* @param {Function} props.getCardDetails - Function to get the details of a card
* @param {Function} props.handleDragStart - Callback function when a drag starts
* @param {Function} props.handleDragEnd - Callback function when a drag ends
* @param {string} props.orientation - The orientation of the lane ("horizontal" or "vertical")
* @param {string} props.className - The CSS class to apply to the lane
* @param {number} props.currentPage - The current page of the lane
* @param {Object} props.otherProps - Any other props to pass to the lane
* @returns {JSX.Element} A lane in a Trello-like board
*/
function Lane({
actions,
id,
@@ -101,20 +78,9 @@ function Lane({
const [currentPageFinal, setCurrentPageFinal] = useState(currentPage);
const [addCardMode, setAddCardMode] = useState(false);
const [collapsed, setCollapsed] = useState(false);
// const [isDraggingOver, setIsDraggingOver] = useState(false);
const laneRef = useRef(null);
const flexStyle = useMemo(() => {
return orientation === "vertical"
? {
display: "flex",
flexWrap: "wrap",
minHeight: "10px"
}
: {};
}, [orientation]);
useEffect(() => {
if (!isEqual(cards, currentPageFinal)) {
setCurrentPageFinal(currentPage);
@@ -222,17 +188,19 @@ function Lane({
collapsibleLanes && setCollapsed(!collapsed);
};
// const groupName = `TrelloBoard${boardId}Lane`;
const renderDragContainer = (isDraggingOver) => {
const stableCards = collapsed ? [] : cards;
const cardList = sortCards(stableCards, laneSortFunction).map((card, idx) => {
const onDeleteCard = () => removeCard(card.id);
const cardToRender = (
const Card = React.memo(({ provided, item: card, isDragging }) => {
const onDeleteCard = () => removeCard(card.id);
return (
<div
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
style={provided.draggableProps.style}
className={`item ${isDragging ? "is-dragging" : ""}`}
key={card.id}
>
<components.Card
key={card.id}
index={idx}
style={card.style || cardStyle}
className="react-trello-card"
onDelete={onDeleteCard}
@@ -244,48 +212,122 @@ function Lane({
editable={editable}
{...card}
/>
);
</div>
);
});
return cardDraggable && (!card.hasOwnProperty("draggable") || card.draggable) ? (
<Draggable key={card.id} draggableId={card.id} index={card.idx}>
{(provided, snapshot) => {
return (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{
...provided.draggableProps.style
}}
>
{cardToRender}
</div>
);
}}
</Draggable>
) : (
<span key={card.id}>{cardToRender}</span>
);
});
const renderDraggable = (index, item) => (
<Draggable draggableId={item.id} index={index} key={item.id}>
{(provided, snapshot) => <Card provided={provided} item={item} isDragging={snapshot.isDragging} />}
</Draggable>
);
const renderAddCardLink = () =>
editable && !addCardMode && <components.AddCardLink onClick={showEditableCard} laneId={id} />;
const renderNewCardForm = () =>
addCardMode && <components.NewCardForm onCancel={hideEditableCard} laneId={id} onAdd={addNewCard} />;
const ItemWrapper = ({ children, ...props }) => (
<div
{...props}
style={{
display: "flex",
flex: 1,
whiteSpace: "nowrap"
}}
>
{children}
</div>
);
const gridComponents = {
List: forwardRef(({ style, children, ...props }, ref) => (
<div
ref={ref}
{...props}
style={{
display: "flex",
flexWrap: "wrap",
...style
}}
>
{children}
</div>
)),
Item: ({ children, ...props }) => (
<div
{...props}
style={{
width: "10%",
display: "flex",
alignContent: "stretch",
boxSizing: "border-box"
}}
>
{children}
</div>
)
};
const renderDroppable = (provided, renderedCards) => {
const Component = orientation === "vertical" ? VirtuosoGrid : Virtuoso;
const commonProps = {
useWindowScroll: true,
data: renderedCards
};
const componentProps =
orientation === "vertical"
? {
...commonProps,
scrollerRef: provided.innerRef,
listClassName: "grid-container",
itemClassName: "grid-item",
components: gridComponents,
itemContent: (index, item) => <ItemWrapper>{renderDraggable(index, item)}</ItemWrapper>
}
: {
...commonProps,
overscan: {
main: 22,
reverse: 22
},
components: { Item: HeightPreservingItem },
itemContent: (index, item) => renderDraggable(index, item),
scrollerRef: provided.innerRef
};
return (
<Droppable direction={orientation === "horizontal" ? "vertical" : "grid"} droppableId={`${index}`} type="lane">
{(provided, snapshot) => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
className={allClassNames}
style={{
...provided.droppableProps.style,
...flexStyle
}}
>
{cardList}
{editable && !addCardMode && <components.AddCardLink onClick={showEditableCard} laneId={id} />}
{addCardMode && <components.NewCardForm onCancel={hideEditableCard} laneId={id} onAdd={addNewCard} />}
{provided.placeholder}
</div>
<div
{...provided.droppableProps}
ref={provided.innerRef}
className={allClassNames}
style={{ ...provided.droppableProps.style }}
>
<Component {...componentProps} />
{renderAddCardLink()}
{renderNewCardForm()}
{provided.placeholder}
</div>
);
};
const renderDragContainer = () => {
if (collapsed) return <></>;
const renderedCards = sortCards(cards, laneSortFunction);
return (
<Droppable
droppableId={id}
type="lane"
direction={orientation === "horizontal" ? "vertical" : "grid"}
mode="virtual"
renderClone={(provided, snapshot, rubric) => (
<Card provided={provided} isDragging={snapshot.isDragging} item={renderedCards[rubric.source.index]} />
)}
>
{(provided) => renderDroppable(provided, renderedCards)}
</Droppable>
);
};

View File

@@ -89,42 +89,47 @@ const LaneHelper = {
},
// TODO: This has been updated to new DND Lib, verified.
moveCardAcrossLanes: (state, { fromLaneId, toLaneId, cardId, index }) => {
// Clone the state to avoid mutation
const newLanes = cloneDeep(state.lanes);
// Find the source and destination lanes using the lane indices
const fromLane = newLanes[fromLaneId];
const toLane = newLanes[toLaneId];
// Find the card in the source lane
const cardIndex = fromLane.cards.findIndex((card) => card.id === cardId);
if (cardIndex === -1) {
throw new Error("Card not found in the source lane");
}
// Remove the card from the source lane
const [card] = fromLane.cards.splice(cardIndex, 1);
// Insert the card into the destination lane at the specified index
toLane.cards.splice(index, 0, card);
let idx = 0;
// Update the lane and card indexes for all lanes
newLanes.forEach((lane, laneIndex) => {
lane.cards.forEach((card, cardIndex) => {
card.idx = idx;
card.laneIndex = laneIndex;
card.cardIndex = cardIndex;
card.laneId = lane.id;
idx++;
});
});
return update(state, {
lanes: { $set: newLanes }
});
moveCardAcrossLanes: (state, ...args) => {
return state;
// console.dir({
// state,
// args: { fromLaneId, toLaneId, cardId, index }
// });
// // Clone the state to avoid mutation
// const newLanes = cloneDeep(state.lanes);
//
// // Find the source and destination lanes using the lane indices
// const fromLane = newLanes[fromLaneId];
// const toLane = newLanes[toLaneId];
//
// // Find the card in the source lane
// const cardIndex = fromLane.cards.findIndex((card) => card.id === cardId);
// if (cardIndex === -1) {
// throw new Error("Card not found in the source lane");
// }
//
// // Remove the card from the source lane
// const [card] = fromLane.cards.splice(cardIndex, 1);
//
// // Insert the card into the destination lane at the specified index
// toLane.cards.splice(index, 0, card);
//
// let idx = 0;
//
// // Update the lane and card indexes for all lanes
// newLanes.forEach((lane, laneIndex) => {
// lane.cards.forEach((card, cardIndex) => {
// card.idx = idx;
// card.laneIndex = laneIndex;
// card.cardIndex = cardIndex;
// card.laneId = lane.id;
// idx++;
// });
// });
//
// return update(state, {
// lanes: { $set: newLanes }
// });
},
updateCardsForLane: (state, { laneId, cards }) => {

View File

@@ -9,10 +9,7 @@ const getBoardWrapperStyles = (props) => {
// TODO: The white-space: nowrap; would be a good place to offer further customization
// This will be put in the lane settings and marked as 'Horizontal Wrapping'
return `
display: flex;
flex-direction: row;
overflow-x: auto;
white-space: nowrap;
white-space: nowrap;
`;
}
return "";
@@ -22,14 +19,13 @@ const getSectionStyles = (props) => {
if (props.orientation === "horizontal") {
return `
display: inline-flex;
position: relative;
flex-direction: column;
white-space: nowrap;
overflow-y: none;
`;
}
return `
margin-bottom: 10px;
position: relative;
flex-direction: column;
`;
};
@@ -80,6 +76,7 @@ export const StyleHorizontal = styled.div`
// TODO: This will need to be changed
min-width: 250px;
min-height: 25px;
margin-bottom: 42px;
}
.react-trello-lane.lane-collapsed {
@@ -99,6 +96,12 @@ export const StyleVertical = styled.div`
.react-trello-board {
display: flex;
}
.grid-container {
}
.grid-item {
}
`;
export const CustomPopoverContainer = styled(PopoverContainer)`
@@ -148,10 +151,9 @@ export const CustomPopoverContent = styled(PopoverContent)`
`;
export const BoardWrapper = styled.div`
background-color: #ffffff;
overflow-y: scroll;
padding: 5px;
color: #393939;
overflow-x: auto;
overflow-y: hidden;
${getBoardWrapperStyles};
`;
@@ -166,7 +168,7 @@ export const Section = styled.section`
background-color: #e3e3e3;
border-radius: 3px;
margin: 2px 2px;
padding: 5px;
padding: 2px;
${getSectionStyles};
`;
@@ -195,7 +197,6 @@ export const LaneFooter = styled.div`
export const ScrollableLane = styled.div`
flex: 1;
overflow-y: auto;
min-width: 250px;
overflow-x: hidden;
align-self: center;