From 7d72d66a972efe826af4a9100254a617975d4641 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Fri, 5 Jul 2024 15:15:28 -0400 Subject: [PATCH] - Lane Checkpoint: In Horizontal mode, all lanes will be the height of the largest lane, this way if you are dragging cards from the bottom of one lane, you do not need to drag to the top of another. Signed-off-by: Dave Richer --- .../production-board-kanban.component.jsx | 2 +- .../production-board-kanban.container.jsx | 73 ++++++++++--------- .../production-board-kanban.styles.scss | 1 - ...roduction-list-columns.alert.component.jsx | 11 ++- .../components/Lane/HeightMemoryWrapper.jsx | 49 +++++++++++++ .../controllers/BoardContainer.jsx | 6 +- .../trello-board/controllers/Lane.jsx | 69 ++++++++++++++---- .../components/trello-board/styles/Base.js | 6 +- 8 files changed, 158 insertions(+), 59 deletions(-) create mode 100644 client/src/components/trello-board/components/Lane/HeightMemoryWrapper.jsx 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 04fba02b8..74fc1d3b0 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 @@ -17,7 +17,7 @@ import ProductionBoardFilters from "../production-board-filters/production-board import ProductionListDetailComponent from "../production-list-detail/production-list-detail.component"; import CardColorLegend from "../production-board-kanban-card/production-board-kanban-card-color-legend.component"; import "./production-board-kanban.styles.scss"; -import { createBoardData } from "./production-board-kanban.utils.js"; +import { createBoardData, createFakeBoardData } from "./production-board-kanban.utils.js"; import ProductionBoardKanbanSettings from "./production-board-kanban.settings.component.jsx"; import cloneDeep from "lodash/cloneDeep"; import isEqual from "lodash/isEqual"; diff --git a/client/src/components/production-board-kanban/production-board-kanban.container.jsx b/client/src/components/production-board-kanban/production-board-kanban.container.jsx index a93f176e2..236cf9099 100644 --- a/client/src/components/production-board-kanban/production-board-kanban.container.jsx +++ b/client/src/components/production-board-kanban/production-board-kanban.container.jsx @@ -1,6 +1,6 @@ import { useApolloClient, useQuery, useSubscription } from "@apollo/client"; import _ from "lodash"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useCallback, useMemo } from "react"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { @@ -19,22 +19,44 @@ const mapStateToProps = createStructuredSelector({ }); export function ProductionBoardKanbanContainer({ bodyshop, currentUser }) { + const client = useApolloClient(); + const [joblist, setJoblist] = useState([]); + const { refetch, loading, data } = useQuery(QUERY_JOBS_IN_PRODUCTION, { pollInterval: 3600000, fetchPolicy: "network-only", nextFetchPolicy: "network-only" }); - const client = useApolloClient(); - const [joblist, setJoblist] = useState([]); + const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION); + const { loading: associationSettingsLoading, data: associationSettings } = useQuery(QUERY_KANBAN_SETTINGS, { + variables: { email: currentUser.email } + }); + + const getUpdatedJobData = useCallback( + async (jobId) => { + await client.query({ + query: QUERY_EXACT_JOB_IN_PRODUCTION, + variables: { id: jobId } + }); + }, + [client] + ); + + const getUpdatedJobsData = useCallback( + async (jobIds) => { + await client.query({ + query: QUERY_EXACT_JOBS_IN_PRODUCTION, + variables: { ids: jobIds } + }); + }, + [client] + ); + useEffect(() => { - if (!(data && data.jobs)) return; - setJoblist( - data.jobs.map((j) => { - return { id: j.id, updated_at: j.updated_at }; - }) - ); + if (!data?.jobs) return; + setJoblist(data.jobs.map((j) => ({ id: j.id, updated_at: j.updated_at }))); }, [data]); useEffect(() => { @@ -46,46 +68,25 @@ export function ProductionBoardKanbanContainer({ bodyshop, currentUser }) { (a, b) => a.id === b.id && a.updated_at === b.updated_at ); - jobDiff.forEach((job) => { - getUpdatedJobData(job.id); - }); if (jobDiff.length > 1) { getUpdatedJobsData(jobDiff.map((j) => j.id)); } else if (jobDiff.length === 1) { - jobDiff.forEach((job) => { - getUpdatedJobData(job.id); - }); + getUpdatedJobData(jobDiff[0].id); } setJoblist(updatedJobs.jobs); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [updatedJobs]); + }, [updatedJobs, joblist, getUpdatedJobData, getUpdatedJobsData]); - const getUpdatedJobData = async (jobId) => { - client.query({ - query: QUERY_EXACT_JOB_IN_PRODUCTION, - variables: { id: jobId } - }); - }; - const getUpdatedJobsData = async (jobIds) => { - client.query({ - query: QUERY_EXACT_JOBS_IN_PRODUCTION, - variables: { ids: jobIds } - }); - }; - - const { loading: associationSettingsLoading, data: associationSettings } = useQuery(QUERY_KANBAN_SETTINGS, { - variables: { email: currentUser.email } - }); + const filteredAssociationSettings = useMemo(() => { + return associationSettings?.associations[0] || null; + }, [associationSettings]); return ( ); } 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 fcc02a2cf..a1b8f9fd1 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 @@ -1,7 +1,6 @@ .react-trello-board { padding: 5px; } - .item .is-dragging { box-shadow: 2px 2px grey; rotate: 5deg; diff --git a/client/src/components/production-list-columns/production-list-columns.alert.component.jsx b/client/src/components/production-list-columns/production-list-columns.alert.component.jsx index c24294c87..e9a4e2d62 100644 --- a/client/src/components/production-list-columns/production-list-columns.alert.component.jsx +++ b/client/src/components/production-list-columns/production-list-columns.alert.component.jsx @@ -2,7 +2,6 @@ import { ExclamationCircleFilled } from "@ant-design/icons"; import { useMutation } from "@apollo/client"; import { Button } from "antd"; import React, { useCallback } from "react"; -import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { logImEXEvent } from "../../firebase/firebase.utils"; @@ -13,11 +12,17 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings"; const mapStateToProps = createStructuredSelector({}); const mapDispatchToProps = (dispatch) => ({ - insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })) + insertAuditTrail: ({ jobid, operation, type }) => + dispatch( + insertAuditTrail({ + jobid, + operation, + type + }) + ) }); const ProductionListColumnAlert = ({ record, insertAuditTrail }) => { - const { t } = useTranslation(); const [updateAlert] = useMutation(UPDATE_JOB); const handleAlertToggle = useCallback(() => { diff --git a/client/src/components/trello-board/components/Lane/HeightMemoryWrapper.jsx b/client/src/components/trello-board/components/Lane/HeightMemoryWrapper.jsx new file mode 100644 index 000000000..e9ea79b79 --- /dev/null +++ b/client/src/components/trello-board/components/Lane/HeightMemoryWrapper.jsx @@ -0,0 +1,49 @@ +import React, { useEffect, useRef } from "react"; +import PropTypes from "prop-types"; + +/** + * Height Memory Wrapper + * @param children + * @param maxHeight + * @param setMaxHeight + * @returns {Element} + * @constructor + */ +const HeightMemoryWrapper = ({ children, maxHeight, setMaxHeight }) => { + const ref = useRef(null); + + useEffect(() => { + const currentRef = ref.current; // Step 1: Capture the current ref value + const updateHeight = () => { + const currentHeight = currentRef?.firstChild?.clientHeight || 0; + setMaxHeight((prevHeight) => Math.max(prevHeight, currentHeight)); + }; + + const resizeObserver = new ResizeObserver(updateHeight); + if (currentRef?.firstChild) { + // Step 2: Use the captured ref for observing + resizeObserver.observe(currentRef.firstChild); + } + + return () => { + if (currentRef?.firstChild) { + // Step 2: Use the captured ref for observing + resizeObserver.unobserve(currentRef.firstChild); + } + }; + }, [setMaxHeight]); + + return ( +
+ {children} +
+ ); +}; + +HeightMemoryWrapper.propTypes = { + children: PropTypes.node.isRequired, + maxHeight: PropTypes.number.isRequired, + setMaxHeight: PropTypes.func.isRequired +}; + +export default HeightMemoryWrapper; diff --git a/client/src/components/trello-board/controllers/BoardContainer.jsx b/client/src/components/trello-board/controllers/BoardContainer.jsx index 8ee046fbb..c11941ee8 100644 --- a/client/src/components/trello-board/controllers/BoardContainer.jsx +++ b/client/src/components/trello-board/controllers/BoardContainer.jsx @@ -15,11 +15,13 @@ import { BoardWrapper } from "../styles/Base.js"; * @component * @param {Object} props - Component props * @param {Object} props.data - The initial data for the board + * @param {Function} props.onDataChange - Callback function when the data changes * @param {Function} props.onDragEnd - Callback function when a drag ends * @param {Function} props.laneSortFunction - Callback function when a drag ends * @param {string} props.orientation - The orientation of the board ("horizontal" or "vertical") * @param {Function} props.eventBusHandle - Function to handle events from the event bus * @param {Object} props.reducerData - The initial data for the Redux reducer + * * @returns {JSX.Element} A Trello-like board */ const BoardContainer = ({ @@ -34,6 +36,7 @@ const BoardContainer = ({ }) => { const [isDragging, setIsDragging] = useState(false); const [isProcessing, setIsProcessing] = useState(false); + const [maxLaneHeight, setMaxLaneHeight] = useState(0); const dispatch = useDispatch(); const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {})); @@ -146,6 +149,8 @@ const BoardContainer = ({ isDragging={isDragging} isProcessing={isProcessing} cardSettings={cardSettings} + maxLaneHeight={maxLaneHeight} + setMaxLaneHeight={setMaxLaneHeight} /> ); })} @@ -157,7 +162,6 @@ const BoardContainer = ({ BoardContainer.propTypes = { id: PropTypes.string, - actions: PropTypes.object, data: PropTypes.object.isRequired, reducerData: PropTypes.object, onDataChange: PropTypes.func, diff --git a/client/src/components/trello-board/controllers/Lane.jsx b/client/src/components/trello-board/controllers/Lane.jsx index e100a00a2..0af0093c7 100644 --- a/client/src/components/trello-board/controllers/Lane.jsx +++ b/client/src/components/trello-board/controllers/Lane.jsx @@ -13,7 +13,25 @@ import { createStructuredSelector } from "reselect"; import { selectBodyshop } from "../../../redux/user/user.selectors.js"; import { selectTechnician } from "../../../redux/tech/tech.selectors.js"; import ProductionBoardCard from "../../production-board-kanban-card/production-board-kanban-card.component.jsx"; +import HeightMemoryWrapper from "../components/Lane/HeightMemoryWrapper.jsx"; +/** + * Lane is a React component that represents a lane in a Trello-like board. + * @param id + * @param title + * @param index + * @param isProcessing + * @param laneSortFunction + * @param cards + * @param cardSettings + * @param orientation + * @param maxLaneHeight + * @param setMaxLaneHeight + * @param technician -- connected to redux + * @param bodyshop -- connected to redux + * @returns {Element} + * @constructor + */ const Lane = ({ id, title, @@ -23,6 +41,8 @@ const Lane = ({ cards, cardSettings = {}, orientation = "vertical", + maxLaneHeight, + setMaxLaneHeight, technician, bodyshop }) => { @@ -92,7 +112,6 @@ const Lane = ({ (provided, renderedCards) => { const Component = orientation === "vertical" ? VirtuosoGrid : Virtuoso; const FinalComponent = collapsed ? "div" : Component; - const commonProps = { useWindowScroll: true, data: renderedCards @@ -119,14 +138,25 @@ const Lane = ({ components: { Item: HeightPreservingItem }, overscan: { main: 3, reverse: 3 }, itemContent: (index, item) => renderDraggable(index, item), - scrollerRef: provided.innerRef + scrollerRef: provided.innerRef, + style: { + minHeight: maxLaneHeight + } }; const componentProps = orientation === "vertical" ? verticalProps : horizontalProps; - const finalComponentProps = collapsed ? {} : componentProps; + const finalComponentProps = collapsed + ? orientation === "horizontal" + ? { + style: { + height: orientation === "vertical" ? 10 : maxLaneHeight + } + } + : {} + : componentProps; - return ( -
+ return orientation === "horizontal" ? ( +
{isVisible && } - {(orientation === "horizontal" || renderedCards.length === 0 || collapsed) && provided.placeholder}
+
+ ) : ( +
+ {isVisible && } + {provided.placeholder}
); }, - [orientation, collapsed, isVisible, renderDraggable] + [orientation, collapsed, isVisible, renderDraggable, maxLaneHeight, setMaxLaneHeight] ); const renderDragContainer = useCallback( @@ -194,15 +233,15 @@ const Lane = ({ Lane.propTypes = { id: PropTypes.string.isRequired, - title: PropTypes.node, - index: PropTypes.number, + title: PropTypes.node.isRequired, + index: PropTypes.number.isRequired, laneSortFunction: PropTypes.func, - cards: PropTypes.array, - orientation: PropTypes.string, - isProcessing: PropTypes.bool, - cardSettings: PropTypes.object, - technician: PropTypes.object, - bodyshop: PropTypes.object + cards: PropTypes.array.isRequired, + orientation: PropTypes.string.isRequired, + isProcessing: PropTypes.bool.isRequired, + cardSettings: PropTypes.object.isRequired, + maxLaneHeight: PropTypes.number.isRequired, + setMaxLaneHeight: PropTypes.func.isRequired }; const mapDispatchToProps = (dispatch) => ({ diff --git a/client/src/components/trello-board/styles/Base.js b/client/src/components/trello-board/styles/Base.js index 376d51729..b17cdb8e8 100644 --- a/client/src/components/trello-board/styles/Base.js +++ b/client/src/components/trello-board/styles/Base.js @@ -1,5 +1,4 @@ -import { PopoverContainer, PopoverContent } from "react-popopo"; -import styled, { css } from "styled-components"; +import styled from "styled-components"; const getBoardWrapperStyles = (props) => { if (props.orientation === "vertical") { @@ -64,6 +63,7 @@ export const StyleHorizontal = styled.div` white-space: nowrap; min-width: 4%; } + .react-trello-column-header { border-radius: 5px; min-height: 15px; @@ -72,6 +72,7 @@ export const StyleHorizontal = styled.div` overflow: hidden; text-overflow: ellipsis; } + .react-trello-card { height: auto; margin: 2px; @@ -88,6 +89,7 @@ export const StyleVertical = styled.div` text-overflow: ellipsis; text-align: left; } + .react-trello-lane { min-height: 5px; height: 100%;