- 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 <dave@imexsystems.ca>
This commit is contained in:
Dave Richer
2024-07-05 15:15:28 -04:00
parent 3454694887
commit 7d72d66a97
8 changed files with 158 additions and 59 deletions

View File

@@ -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";

View File

@@ -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 (
<ProductionBoardKanbanComponent
loading={loading || associationSettingsLoading}
data={data ? data.jobs : []}
refetch={refetch}
associationSettings={
associationSettings && associationSettings.associations[0] ? associationSettings.associations[0] : null
}
associationSettings={filteredAssociationSettings}
/>
);
}

View File

@@ -1,7 +1,6 @@
.react-trello-board {
padding: 5px;
}
.item .is-dragging {
box-shadow: 2px 2px grey;
rotate: 5deg;

View File

@@ -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(() => {

View File

@@ -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 (
<div ref={ref} style={{ minHeight: maxHeight }}>
{children}
</div>
);
};
HeightMemoryWrapper.propTypes = {
children: PropTypes.node.isRequired,
maxHeight: PropTypes.number.isRequired,
setMaxHeight: PropTypes.func.isRequired
};
export default HeightMemoryWrapper;

View File

@@ -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,

View File

@@ -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 (
<div style={{ height: "100%" }}>
return orientation === "horizontal" ? (
<HeightMemoryWrapper maxHeight={maxLaneHeight} setMaxHeight={setMaxLaneHeight}>
<div
{...provided.droppableProps}
ref={provided.innerRef}
@@ -134,12 +164,21 @@ const Lane = ({
style={{ ...provided.droppableProps.style }}
>
{isVisible && <FinalComponent {...finalComponentProps} />}
{(orientation === "horizontal" || renderedCards.length === 0 || collapsed) && provided.placeholder}
</div>
</HeightMemoryWrapper>
) : (
<div
{...provided.droppableProps}
ref={provided.innerRef}
className={`react-trello-lane ${collapsed ? "lane-collapsed" : ""}`}
style={{ ...provided.droppableProps.style }}
>
{isVisible && <FinalComponent {...finalComponentProps} />}
{provided.placeholder}
</div>
);
},
[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) => ({

View File

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