From 2f493c63f8dcd03ef3956c9671c0ba2273e74701 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Fri, 28 Jun 2024 15:08:26 -0400 Subject: [PATCH] - Clear stage prior to implementing replacement for collapsed lanes (with virtual lists) Signed-off-by: Dave Richer --- .../production-board-kanban.component.jsx | 252 +++++++---------- .../production-board-kanban.styles.scss | 28 +- .../trello-board/controllers/Lane.jsx | 256 ++++++++++-------- .../trello-board/helpers/LaneHelper.js | 77 +++--- .../components/trello-board/styles/Base.js | 25 +- 5 files changed, 313 insertions(+), 325 deletions(-) diff --git a/client/src/components/production-board-kanban/production-board-kanban.component.jsx b/client/src/components/production-board-kanban/production-board-kanban.component.jsx index aebc697a4..4f5b3910c 100644 --- a/client/src/components/production-board-kanban/production-board-kanban.component.jsx +++ b/client/src/components/production-board-kanban/production-board-kanban.component.jsx @@ -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 }) => ( - - {({ style }) => ( -
- {title} -
- )} -
- ); - - const NormalHeader = ({ title }) => ( + const Header = ({ title }) => (
{title}
@@ -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 ( - +
{cardSettings.cardcolor && } - {cardSettings.stickyheader ? ( - - - - ) : ( -
- -
- )} - + +
); } 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; - } -`; diff --git a/client/src/components/production-board-kanban/production-board-kanban.styles.scss b/client/src/components/production-board-kanban/production-board-kanban.styles.scss index 92666e1a5..1657d398e 100644 --- a/client/src/components/production-board-kanban/production-board-kanban.styles.scss +++ b/client/src/components/production-board-kanban/production-board-kanban.styles.scss @@ -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; diff --git a/client/src/components/trello-board/controllers/Lane.jsx b/client/src/components/trello-board/controllers/Lane.jsx index c97175488..f87ebfb50 100644 --- a/client/src/components/trello-board/controllers/Lane.jsx +++ b/client/src/components/trello-board/controllers/Lane.jsx @@ -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 ( +
+ {children} +
+ ); +} -/** - * 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 ( +
- ); +
+ ); + }); - return cardDraggable && (!card.hasOwnProperty("draggable") || card.draggable) ? ( - - {(provided, snapshot) => { - return ( -
- {cardToRender} -
- ); - }} -
- ) : ( - {cardToRender} - ); - }); + const renderDraggable = (index, item) => ( + + {(provided, snapshot) => } + + ); + + const renderAddCardLink = () => + editable && !addCardMode && ; + + const renderNewCardForm = () => + addCardMode && ; + + const ItemWrapper = ({ children, ...props }) => ( +
+ {children} +
+ ); + + const gridComponents = { + List: forwardRef(({ style, children, ...props }, ref) => ( +
+ {children} +
+ )), + Item: ({ children, ...props }) => ( +
+ {children} +
+ ) + }; + + 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) => {renderDraggable(index, item)} + } + : { + ...commonProps, + overscan: { + main: 22, + reverse: 22 + }, + components: { Item: HeightPreservingItem }, + itemContent: (index, item) => renderDraggable(index, item), + scrollerRef: provided.innerRef + }; return ( - - {(provided, snapshot) => ( -
- {cardList} - {editable && !addCardMode && } - {addCardMode && } - {provided.placeholder} -
+
+ + {renderAddCardLink()} + {renderNewCardForm()} + {provided.placeholder} +
+ ); + }; + + const renderDragContainer = () => { + if (collapsed) return <>; + const renderedCards = sortCards(cards, laneSortFunction); + return ( + ( + )} + > + {(provided) => renderDroppable(provided, renderedCards)} ); }; diff --git a/client/src/components/trello-board/helpers/LaneHelper.js b/client/src/components/trello-board/helpers/LaneHelper.js index 071ba2ad3..d84d1c16c 100644 --- a/client/src/components/trello-board/helpers/LaneHelper.js +++ b/client/src/components/trello-board/helpers/LaneHelper.js @@ -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 }) => { diff --git a/client/src/components/trello-board/styles/Base.js b/client/src/components/trello-board/styles/Base.js index bee24c08c..b9f3f791a 100644 --- a/client/src/components/trello-board/styles/Base.js +++ b/client/src/components/trello-board/styles/Base.js @@ -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;