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%;