feature/IO-3499-React-19-ProductionBoard - Production Board React 19 Updates

This commit is contained in:
Dave
2026-01-14 15:00:24 -05:00
parent be42eae5a3
commit a68e52234a
14 changed files with 828 additions and 667 deletions

View File

@@ -7,7 +7,6 @@ import {
} from "@ant-design/icons";
import { Card, Col, Row, Space, Tooltip } from "antd";
import Dinero from "dinero.js";
import { memo, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { DateTimeFormatter } from "../../utils/DateFormatter";
@@ -45,7 +44,7 @@ const getContrastYIQ = (bgColor, isDarkMode = document.documentElement.getAttrib
const findEmployeeById = (employees, id) => employees.find((e) => e.id === id);
const EllipsesToolTip = memo(({ title, children, kiosk }) => {
function EllipsesToolTip({ title, children, kiosk }) {
if (kiosk || !title) {
return <div className="ellipses no-select">{children}</div>;
}
@@ -54,9 +53,7 @@ const EllipsesToolTip = memo(({ title, children, kiosk }) => {
<div className="ellipses">{children}</div>
</Tooltip>
);
});
EllipsesToolTip.displayName = "EllipsesToolTip";
}
const OwnerNameToolTip = ({ metadata, cardSettings }) =>
cardSettings?.ownr_nm && (
@@ -330,47 +327,47 @@ const PartsReceivedComponent = ({ metadata, cardSettings, card }) =>
export default function ProductionBoardCard({ technician, card, bodyshop, cardSettings }) {
const { t } = useTranslation();
const { metadata } = card;
const employees = useMemo(() => bodyshop.employees, [bodyshop.employees]);
const { employee_body, employee_prep, employee_refinish, employee_csr } = useMemo(() => {
return {
employee_body: metadata?.employee_body && findEmployeeById(employees, metadata.employee_body),
employee_prep: metadata?.employee_prep && findEmployeeById(employees, metadata.employee_prep),
employee_refinish: metadata?.employee_refinish && findEmployeeById(employees, metadata.employee_refinish),
employee_csr: metadata?.employee_csr && findEmployeeById(employees, metadata.employee_csr)
};
}, [metadata, employees]);
const pastDueAlert = useMemo(() => {
if (!metadata?.scheduled_completion) return null;
const employees = bodyshop.employees;
const employee_body = metadata?.employee_body && findEmployeeById(employees, metadata.employee_body);
const employee_prep = metadata?.employee_prep && findEmployeeById(employees, metadata.employee_prep);
const employee_refinish = metadata?.employee_refinish && findEmployeeById(employees, metadata.employee_refinish);
const employee_csr = metadata?.employee_csr && findEmployeeById(employees, metadata.employee_csr);
let pastDueAlert = null;
if (metadata?.scheduled_completion) {
const completionDate = dayjs(metadata.scheduled_completion);
if (dayjs().isSameOrAfter(completionDate, "day")) return "production-completion-past";
if (dayjs().add(1, "day").isSame(completionDate, "day")) return "production-completion-soon";
return null;
}, [metadata?.scheduled_completion]);
const totalHrs = useMemo(() => {
return metadata?.labhrs && metadata?.larhrs
if (dayjs().isSameOrAfter(completionDate, "day")) {
pastDueAlert = "production-completion-past";
} else if (dayjs().add(1, "day").isSame(completionDate, "day")) {
pastDueAlert = "production-completion-soon";
}
}
const totalHrs =
metadata?.labhrs && metadata?.larhrs
? metadata.labhrs.aggregate.sum.mod_lb_hrs + metadata.larhrs.aggregate.sum.mod_lb_hrs
: 0;
}, [metadata?.labhrs, metadata?.larhrs]);
const bgColor = useMemo(() => cardColor(bodyshop.ssbuckets, totalHrs), [bodyshop.ssbuckets, totalHrs]);
const contrastYIQ = useMemo(() => getContrastYIQ(bgColor), [bgColor]);
const isBodyEmpty = useMemo(() => {
return !(
cardSettings?.ownr_nm ||
cardSettings?.model_info ||
cardSettings?.ins_co_nm ||
cardSettings?.clm_no ||
cardSettings?.employeeassignments ||
cardSettings?.actual_in ||
cardSettings?.scheduled_completion ||
cardSettings?.ats ||
cardSettings?.sublets ||
cardSettings?.production_note ||
cardSettings?.partsstatus ||
cardSettings?.estimator ||
cardSettings?.subtotal ||
cardSettings?.tasks
);
}, [cardSettings]);
const bgColor = cardColor(bodyshop.ssbuckets, totalHrs);
const contrastYIQ = getContrastYIQ(bgColor);
const isBodyEmpty = !(
cardSettings?.ownr_nm ||
cardSettings?.model_info ||
cardSettings?.ins_co_nm ||
cardSettings?.clm_no ||
cardSettings?.employeeassignments ||
cardSettings?.actual_in ||
cardSettings?.scheduled_completion ||
cardSettings?.ats ||
cardSettings?.sublets ||
cardSettings?.production_note ||
cardSettings?.partsstatus ||
cardSettings?.estimator ||
cardSettings?.subtotal ||
cardSettings?.tasks
);
const headerContent = (
<div className="header-content-container">

View File

@@ -2,9 +2,7 @@ import { SyncOutlined } from "@ant-design/icons";
import { PageHeader } from "@ant-design/pro-layout";
import { useApolloClient } from "@apollo/client/react";
import { Button, Skeleton, Space } from "antd";
import cloneDeep from "lodash/cloneDeep";
import isEqual from "lodash/isEqual";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -74,17 +72,11 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr
title: `${lane.title} (${lane.cards.length})`
}));
setBoardLanes((prevBoardLanes) => {
const deepClonedData = cloneDeep(newBoardData);
if (!isEqual(prevBoardLanes, deepClonedData)) {
return deepClonedData;
}
return prevBoardLanes;
});
setBoardLanes(newBoardData);
setIsMoving(false);
}, [data, bodyshop.md_ro_statuses, filter, statuses, associationSettings?.kanban_settings]);
const getCardByID = useCallback((data, cardId) => {
const getCardByID = (data, cardId) => {
for (const lane of data.lanes) {
for (const card of lane.cards) {
if (card.id === cardId) {
@@ -93,102 +85,96 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr
}
}
return null;
}, []);
};
const onDragEnd = useCallback(
async ({ type, source, destination, draggableId }) => {
logImEXEvent("kanban_drag_end");
const onDragEnd = async ({ type, source, destination, draggableId }) => {
logImEXEvent("kanban_drag_end");
if (!type || type !== "lane" || !source || !destination || isMoving) return;
if (!type || type !== "lane" || !source || !destination || isMoving) return;
setIsMoving(true);
setIsMoving(true);
const targetLane = boardLanes.lanes.find((lane) => lane.id === destination.droppableId);
const sourceLane = boardLanes.lanes.find((lane) => lane.id === source.droppableId);
const targetLane = boardLanes.lanes.find((lane) => lane.id === destination.droppableId);
const sourceLane = boardLanes.lanes.find((lane) => lane.id === source.droppableId);
if (!targetLane || !sourceLane) {
setIsMoving(false);
console.error("Invalid source or destination lane");
return;
}
if (!targetLane || !sourceLane) {
setIsMoving(false);
console.error("Invalid source or destination lane");
return;
}
const sameColumnTransfer = source.droppableId === destination.droppableId;
const sourceCard = getCardByID(boardLanes, draggableId);
const sameColumnTransfer = source.droppableId === destination.droppableId;
const sourceCard = getCardByID(boardLanes, draggableId);
const movedCardWillBeFirst = destination.index === 0;
const movedCardWillBeLast = destination.index >= targetLane.cards.length - 1;
const movedCardWillBeFirst = destination.index === 0;
const movedCardWillBeLast = destination.index >= targetLane.cards.length - 1;
const lastCardInTargetLane = targetLane.cards[targetLane.cards.length - 1];
const oldChildCard = sourceLane.cards[source.index + 1];
const lastCardInTargetLane = targetLane.cards[targetLane.cards.length - 1];
const oldChildCard = sourceLane.cards[source.index + 1];
const newChildCard = movedCardWillBeLast
? null
: targetLane.cards[
sameColumnTransfer
? source.index < destination.index
? destination.index + 1
: destination.index
const newChildCard = movedCardWillBeLast
? null
: targetLane.cards[
sameColumnTransfer
? source.index < destination.index
? destination.index + 1
: destination.index
];
: destination.index
];
const oldChildCardNewParent = oldChildCard ? sourceCard.metadata.kanbanparent : null;
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.error("==> !!!!!!Couldn't find a parent.!!!! <==");
}
let movedCardNewKanbanParent;
if (movedCardWillBeFirst) {
movedCardNewKanbanParent = "-1";
} else if (movedCardWillBeLast) {
movedCardNewKanbanParent = lastCardInTargetLane.id;
} else if (newChildCard) {
movedCardNewKanbanParent = newChildCard.metadata.kanbanparent;
} else {
console.error("==> !!!!!!Couldn't find a parent.!!!! <==");
}
const newChildCardNewParent = newChildCard ? draggableId : null;
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
)
});
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"
});
insertAuditTrail({
jobid: draggableId,
operation: AuditTrailMapping.jobstatuschange(targetLane.id),
type: "jobstatuschange"
});
if (update.errors) {
notification.error({
title: t("production.errors.boardupdate", {
message: JSON.stringify(update.errors)
})
});
}
} catch (error) {
if (update.errors) {
notification.error({
title: t("production.errors.boardupdate", {
message: error.message
message: JSON.stringify(update.errors)
})
});
} finally {
setIsMoving(false);
}
},
[boardLanes, client, getCardByID, isMoving, t, insertAuditTrail, notification]
);
} catch (error) {
notification.error({
title: t("production.errors.boardupdate", {
message: error.message
})
});
} finally {
setIsMoving(false);
}
};
const cardSettings = useMemo(() => {
const kanbanSettings = associationSettings?.kanban_settings;
return mergeWithDefaults(kanbanSettings);
}, [associationSettings?.kanban_settings]);
const cardSettings = mergeWithDefaults(associationSettings?.kanban_settings);
const handleSettingsChange = () => {
setFilter(defaultFilters);

View File

@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef } from "react";
import { useEffect, useRef } from "react";
import { useApolloClient, useQuery, useSubscription } from "@apollo/client/react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -35,13 +35,10 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionTyp
splitKey: bodyshop && bodyshop.imexshopid
});
const combinedStatuses = useMemo(
() => [
...bodyshop.md_ro_statuses.production_statuses,
...(bodyshop.md_ro_statuses.additional_board_statuses || [])
],
[bodyshop.md_ro_statuses.production_statuses, bodyshop.md_ro_statuses.additional_board_statuses]
);
const combinedStatuses = [
...bodyshop.md_ro_statuses.production_statuses,
...(bodyshop.md_ro_statuses.additional_board_statuses || [])
];
const { refetch, loading, data } = useQuery(QUERY_JOBS_IN_PRODUCTION, {
pollInterval: 3600000,
@@ -168,9 +165,7 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionTyp
};
}, [subscriptionEnabled, socket, bodyshop, client, refetch]);
const filteredAssociationSettings = useMemo(() => {
return associationSettings?.associations[0] || null;
}, [associationSettings?.associations]);
const filteredAssociationSettings = associationSettings?.associations[0] || null;
return (
<ProductionBoardKanbanComponent

View File

@@ -1,4 +1,3 @@
import { useMemo } from "react";
import { Card, Statistic } from "antd";
import { useTranslation } from "react-i18next";
import PropTypes from "prop-types";
@@ -68,128 +67,85 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
return value;
};
const totalHrs = useMemo(() => {
if (!cardSettings.totalHrs) return null;
const total = calculateTotal(data, "labhrs", "mod_lb_hrs") + calculateTotal(data, "larhrs", "mod_lb_hrs");
return parseFloat(total.toFixed(2));
}, [data, cardSettings.totalHrs]);
const totalHrs = cardSettings.totalHrs
? parseFloat((calculateTotal(data, "labhrs", "mod_lb_hrs") + calculateTotal(data, "larhrs", "mod_lb_hrs")).toFixed(2))
: null;
const totalLAB = useMemo(() => {
if (!cardSettings.totalLAB) return null;
const total = calculateTotal(data, "labhrs", "mod_lb_hrs");
return parseFloat(total.toFixed(2));
}, [data, cardSettings.totalLAB]);
const totalLAB = cardSettings.totalLAB
? parseFloat(calculateTotal(data, "labhrs", "mod_lb_hrs").toFixed(2))
: null;
const totalLAR = useMemo(() => {
if (!cardSettings.totalLAR) return null;
const total = calculateTotal(data, "larhrs", "mod_lb_hrs");
return parseFloat(total.toFixed(2));
}, [data, cardSettings.totalLAR]);
const totalLAR = cardSettings.totalLAR
? parseFloat(calculateTotal(data, "larhrs", "mod_lb_hrs").toFixed(2))
: null;
const jobsInProduction = useMemo(
() => (cardSettings.jobsInProduction ? data.length : null),
[data, cardSettings.jobsInProduction]
);
const jobsInProduction = cardSettings.jobsInProduction ? data.length : null;
const totalAmountInProduction = useMemo(() => {
if (!cardSettings.totalAmountInProduction) return null;
const total = calculateTotalAmount(data, "job_totals");
return total.toFormat("$0,0.00");
}, [data, cardSettings.totalAmountInProduction]);
const totalAmountInProduction = cardSettings.totalAmountInProduction
? calculateTotalAmount(data, "job_totals").toFormat("$0,0.00")
: null;
const totalAmountOnBoard = useMemo(() => {
if (!reducerData || !cardSettings.totalAmountOnBoard) return null;
const total = calculateReducerTotalAmount(reducerData.lanes, "job_totals");
return total.toFormat("$0,0.00");
}, [reducerData, cardSettings.totalAmountOnBoard]);
const totalAmountOnBoard = reducerData && cardSettings.totalAmountOnBoard
? calculateReducerTotalAmount(reducerData.lanes, "job_totals").toFormat("$0,0.00")
: null;
const totalHrsOnBoard = useMemo(() => {
if (!reducerData || !cardSettings.totalHrsOnBoard) return null;
const total =
calculateReducerTotal(reducerData.lanes, "labhrs", "mod_lb_hrs") +
calculateReducerTotal(reducerData.lanes, "larhrs", "mod_lb_hrs");
return parseFloat(total.toFixed(2));
}, [reducerData, cardSettings.totalHrsOnBoard]);
const totalHrsOnBoard = reducerData && cardSettings.totalHrsOnBoard
? parseFloat((
calculateReducerTotal(reducerData.lanes, "labhrs", "mod_lb_hrs") +
calculateReducerTotal(reducerData.lanes, "larhrs", "mod_lb_hrs")
).toFixed(2))
: null;
const totalLABOnBoard = useMemo(() => {
if (!reducerData || !cardSettings.totalLABOnBoard) return null;
const total = calculateReducerTotal(reducerData.lanes, "labhrs", "mod_lb_hrs");
return parseFloat(total.toFixed(2));
}, [reducerData, cardSettings.totalLABOnBoard]);
const totalLABOnBoard = reducerData && cardSettings.totalLABOnBoard
? parseFloat(calculateReducerTotal(reducerData.lanes, "labhrs", "mod_lb_hrs").toFixed(2))
: null;
const totalLAROnBoard = useMemo(() => {
if (!reducerData || !cardSettings.totalLAROnBoard) return null;
const total = calculateReducerTotal(reducerData.lanes, "larhrs", "mod_lb_hrs");
return parseFloat(total.toFixed(2));
}, [reducerData, cardSettings.totalLAROnBoard]);
const totalLAROnBoard = reducerData && cardSettings.totalLAROnBoard
? parseFloat(calculateReducerTotal(reducerData.lanes, "larhrs", "mod_lb_hrs").toFixed(2))
: null;
const jobsOnBoard = useMemo(
() =>
reducerData && cardSettings.jobsOnBoard
? reducerData.lanes.reduce((acc, lane) => acc + lane.cards.length, 0)
: null,
[reducerData, cardSettings.jobsOnBoard]
);
const jobsOnBoard = reducerData && cardSettings.jobsOnBoard
? reducerData.lanes.reduce((acc, lane) => acc + lane.cards.length, 0)
: null;
const tasksInProduction = useMemo(() => {
if (!data || !cardSettings.tasksInProduction) return null;
return data.reduce((acc, item) => acc + (item.tasks_aggregate?.aggregate?.count || 0), 0);
}, [data, cardSettings.tasksInProduction]);
const tasksInProduction = cardSettings.tasksInProduction
? data.reduce((acc, item) => acc + (item.tasks_aggregate?.aggregate?.count || 0), 0)
: null;
const tasksOnBoard = useMemo(() => {
if (!reducerData || !cardSettings.tasksOnBoard) return null;
return reducerData.lanes.reduce((acc, lane) => {
return (
acc + lane.cards.reduce((laneAcc, card) => laneAcc + (card.metadata.tasks_aggregate?.aggregate?.count || 0), 0)
);
}, 0);
}, [reducerData, cardSettings.tasksOnBoard]);
const tasksOnBoard = reducerData && cardSettings.tasksOnBoard
? reducerData.lanes.reduce((acc, lane) => {
return (
acc + lane.cards.reduce((laneAcc, card) => laneAcc + (card.metadata.tasks_aggregate?.aggregate?.count || 0), 0)
);
}, 0)
: null;
const statistics = useMemo(
() =>
mergeStatistics(statisticsItems, [
{ id: 0, value: totalHrs, type: StatisticType.HOURS },
{ id: 1, value: totalAmountInProduction, type: StatisticType.AMOUNT },
{ id: 2, value: totalLAB, type: StatisticType.HOURS },
{ id: 3, value: totalLAR, type: StatisticType.HOURS },
{ id: 4, value: jobsInProduction, type: StatisticType.JOBS },
{ id: 5, value: totalHrsOnBoard, type: StatisticType.HOURS },
{ id: 6, value: totalAmountOnBoard, type: StatisticType.AMOUNT },
{ id: 7, value: totalLABOnBoard, type: StatisticType.HOURS },
{ id: 8, value: totalLAROnBoard, type: StatisticType.HOURS },
{ id: 9, value: jobsOnBoard, type: StatisticType.JOBS },
{ id: 10, value: tasksOnBoard, type: StatisticType.TASKS },
{ id: 11, value: tasksInProduction, type: StatisticType.TASKS }
]),
[
totalHrs,
totalAmountInProduction,
totalLAB,
totalLAR,
jobsInProduction,
totalHrsOnBoard,
totalAmountOnBoard,
totalLABOnBoard,
totalLAROnBoard,
jobsOnBoard,
tasksOnBoard,
tasksInProduction
]
);
const statistics = mergeStatistics(statisticsItems, [
{ id: 0, value: totalHrs, type: StatisticType.HOURS },
{ id: 1, value: totalAmountInProduction, type: StatisticType.AMOUNT },
{ id: 2, value: totalLAB, type: StatisticType.HOURS },
{ id: 3, value: totalLAR, type: StatisticType.HOURS },
{ id: 4, value: jobsInProduction, type: StatisticType.JOBS },
{ id: 5, value: totalHrsOnBoard, type: StatisticType.HOURS },
{ id: 6, value: totalAmountOnBoard, type: StatisticType.AMOUNT },
{ id: 7, value: totalLABOnBoard, type: StatisticType.HOURS },
{ id: 8, value: totalLAROnBoard, type: StatisticType.HOURS },
{ id: 9, value: jobsOnBoard, type: StatisticType.JOBS },
{ id: 10, value: tasksOnBoard, type: StatisticType.TASKS },
{ id: 11, value: tasksInProduction, type: StatisticType.TASKS }
]);
const sortedStatistics = useMemo(() => {
const statisticsMap = new Map(statistics.map((stat) => [stat.id, stat]));
const statisticsMap = new Map(statistics.map((stat) => [stat.id, stat]));
return (
cardSettings?.statisticsOrder ? cardSettings.statisticsOrder : defaultKanbanSettings.statisticsOrder
).reduce((sorted, orderId) => {
const value = statisticsMap.get(orderId);
if (value?.value) {
sorted.push(value);
}
return sorted;
}, []);
}, [statistics, cardSettings.statisticsOrder]);
const sortedStatistics = (
cardSettings?.statisticsOrder ? cardSettings.statisticsOrder : defaultKanbanSettings.statisticsOrder
).reduce((sorted, orderId) => {
const value = statisticsMap.get(orderId);
if (value?.value) {
sorted.push(value);
}
return sorted;
}, []);
return (
<div style={{ display: "flex", gap: "5px", flexWrap: "wrap", marginBottom: "5px" }}>

View File

@@ -1,11 +1,9 @@
import { memo } from "react";
const ItemWrapper = memo(({ children, ...props }) => (
<div {...props} className="item-wrapper">
{children}
</div>
));
ItemWrapper.displayName = "ItemWrapper";
function ItemWrapper({ children, ...props }) {
return (
<div {...props} className="item-wrapper">
{children}
</div>
);
}
export default ItemWrapper;

View File

@@ -1,38 +1,34 @@
import { BoardContainer } from "../index";
import { useMemo } from "react";
import { StyleHorizontal, StyleVertical } from "../styles/Base.js";
import { cardSizesVertical } from "../styles/Globals.js";
const Board = ({ orientation, cardSettings, ...additionalProps }) => {
const OrientationStyle = useMemo(
() => (orientation === "horizontal" ? StyleHorizontal : StyleVertical),
[orientation]
);
const OrientationStyle = orientation === "horizontal" ? StyleHorizontal : StyleVertical;
const gridItemWidth = useMemo(() => {
switch (cardSettings?.cardSize) {
case "small":
return cardSizesVertical.small;
case "large":
return cardSizesVertical.large;
case "medium":
return cardSizesVertical.medium;
default:
return cardSizesVertical.small;
}
}, [cardSettings?.cardSize]);
let gridItemWidth;
switch (cardSettings?.cardSize) {
case "small":
gridItemWidth = cardSizesVertical.small;
break;
case "large":
gridItemWidth = cardSizesVertical.large;
break;
case "medium":
gridItemWidth = cardSizesVertical.medium;
break;
default:
gridItemWidth = cardSizesVertical.small;
}
return (
<>
<OrientationStyle {...{ gridItemWidth }}>
<BoardContainer
orientation={orientation}
cardSettings={cardSettings}
{...additionalProps}
className="react-trello-board"
/>
</OrientationStyle>
</>
<OrientationStyle {...{ gridItemWidth }}>
<BoardContainer
orientation={orientation}
cardSettings={cardSettings}
{...additionalProps}
className="react-trello-board"
/>
</OrientationStyle>
);
};

View File

@@ -1,8 +1,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { DragDropContext } from "../dnd/lib";
import PropTypes from "prop-types";
import isEqual from "lodash/isEqual";
import Lane from "./Lane";
import { PopoverWrapper } from "react-popopo";
import * as actions from "../../../../redux/trello/trello.actions.js";
@@ -37,7 +36,6 @@ const BoardContainer = ({
orientation = "horizontal",
cardSettings = {},
eventBusHandle,
reducerData,
queryData
}) => {
const [isDragging, setIsDragging] = useState(false);
@@ -50,24 +48,10 @@ const BoardContainer = ({
const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {}));
const { setDragTime, getLastDragTime } = useDragMap();
const wireEventBus = useCallback(() => {
const wireEventBus = () => {
const eventBus = {
publish: (event) => {
switch (event.type) {
// case "ADD_CARD":
// return dispatch(actions.addCard({ laneId: event.laneId, card: event.card }));
// case "REMOVE_CARD":
// return dispatch(actions.removeCard({ laneId: event.laneId, cardId: event.cardId }));
// case "REFRESH_BOARD":
// return dispatch(actions.loadBoard(event.data));
// case "UPDATE_CARDS":
// return dispatch(actions.updateCards({ laneId: event.laneId, cards: event.cards }));
// case "UPDATE_CARD":
// return dispatch(actions.updateCard({ laneId: event.laneId, updatedCard: event.card }));
// case "UPDATE_LANES":
// return dispatch(actions.updateLanes(event.lanes));
// case "UPDATE_LANE":
// return dispatch(actions.updateLane(event.lane));
case "MOVE_CARD":
return dispatch(
actions.moveCardAcrossLanes({
@@ -84,66 +68,30 @@ const BoardContainer = ({
}
};
eventBusHandle(eventBus);
}, [dispatch, eventBusHandle]);
};
useEffect(() => {
dispatch(actions.loadBoard(data));
if (eventBusHandle) {
wireEventBus();
}
}, [data, eventBusHandle, dispatch, wireEventBus]);
}, [data, eventBusHandle, dispatch]);
useEffect(() => {
if (!isEqual(currentReducerData, reducerData)) {
onDataChange(currentReducerData);
}
}, [currentReducerData, reducerData, onDataChange]);
onDataChange(currentReducerData);
}, [currentReducerData, onDataChange]);
const onDragStart = useCallback(() => {
const onDragStart = () => {
setIsDragging(true);
}, []);
};
const onLaneDrag = useCallback(
async ({ draggableId, type, source, reason, mode, destination, combine }) => {
setIsDragging(false);
const onLaneDrag = async ({ draggableId, type, source, reason, mode, destination, combine }) => {
setIsDragging(false);
// Validate drag type and source
if (type !== "lane" || !source) {
// Invalid drag type or missing source, attempt to revert if possible
if (source) {
dispatch(
actions.moveCardAcrossLanes({
fromLaneId: source.droppableId,
toLaneId: source.droppableId,
cardId: draggableId,
index: source.index
})
);
}
setIsProcessing(false);
try {
await onDragEnd({ draggableId, type, source, reason, mode, destination, combine });
} catch (err) {
console.error("Error in onLaneDrag for invalid drag type or source", err);
}
return;
}
setDragTime(source.droppableId);
setIsProcessing(true);
// Handle valid drop to a different lane or position
if (destination && !isEqual(source, destination)) {
dispatch(
actions.moveCardAcrossLanes({
fromLaneId: source.droppableId,
toLaneId: destination.droppableId,
cardId: draggableId,
index: destination.index
})
);
} else {
// Same-lane drop or no destination, revert to original position
// Validate drag type and source
if (type !== "lane" || !source) {
// Invalid drag type or missing source, attempt to revert if possible
if (source) {
dispatch(
actions.moveCardAcrossLanes({
fromLaneId: source.droppableId,
@@ -153,26 +101,57 @@ const BoardContainer = ({
})
);
}
setIsProcessing(false);
try {
await onDragEnd({ draggableId, type, source, reason, mode, destination, combine });
} catch (err) {
console.error("Error in onLaneDrag", err);
// Ensure revert on error
dispatch(
actions.moveCardAcrossLanes({
fromLaneId: source.droppableId,
toLaneId: source.droppableId,
cardId: draggableId,
index: source.index
})
);
} finally {
setIsProcessing(false);
console.error("Error in onLaneDrag for invalid drag type or source", err);
}
},
[dispatch, onDragEnd, setDragTime]
);
return;
}
setDragTime(source.droppableId);
setIsProcessing(true);
// Handle valid drop to a different lane or position
if (destination && (source.droppableId !== destination.droppableId || source.index !== destination.index)) {
dispatch(
actions.moveCardAcrossLanes({
fromLaneId: source.droppableId,
toLaneId: destination.droppableId,
cardId: draggableId,
index: destination.index
})
);
} else {
// Same-lane drop or no destination, revert to original position
dispatch(
actions.moveCardAcrossLanes({
fromLaneId: source.droppableId,
toLaneId: source.droppableId,
cardId: draggableId,
index: source.index
})
);
}
try {
await onDragEnd({ draggableId, type, source, reason, mode, destination, combine });
} catch (err) {
console.error("Error in onLaneDrag", err);
// Ensure revert on error
dispatch(
actions.moveCardAcrossLanes({
fromLaneId: source.droppableId,
toLaneId: source.droppableId,
cardId: draggableId,
index: source.index
})
);
} finally {
setIsProcessing(false);
}
};
return (
<div>

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo, useRef, useState } from "react";
import { useRef, useState } from "react";
import PropTypes from "prop-types";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
@@ -64,185 +64,162 @@ const Lane = ({
const [collapsed, setCollapsed] = useState(false);
const laneRef = useRef(null);
const sortedCards = useMemo(() => {
if (!cards) return [];
if (!laneSortFunction) return cards;
return [...cards].sort(laneSortFunction);
}, [cards, laneSortFunction]);
let sortedCards = cards || [];
if (laneSortFunction && cards) {
sortedCards = [...cards].sort(laneSortFunction);
}
const toggleLaneCollapsed = useCallback(() => {
const toggleLaneCollapsed = () => {
setCollapsed((prevCollapsed) => !prevCollapsed);
}, []);
};
const renderDraggable = useCallback(
(index, card) => {
if (!card) {
console.log("null card");
return null;
}
return (
<Draggable draggableId={card.id} index={index} key={card.id} isDragDisabled={isProcessing}>
{(provided, snapshot) => (
<div
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
style={provided.draggableProps.style}
className={`item ${snapshot.isDragging ? "is-dragging" : ""}`}
key={card.id}
>
<SizeMemoryWrapper
maxHeight={maxCardHeight}
setMaxHeight={setMaxCardHeight}
maxWidth={maxCardWidth}
setMaxWidth={setMaxCardWidth}
>
<ProductionBoardCard
technician={technician}
bodyshop={bodyshop}
cardSettings={cardSettings}
key={card.id}
card={card}
style={{ minHeight: maxCardHeight, minWidth: maxCardWidth }}
className="react-trello-card"
/>
</SizeMemoryWrapper>
</div>
)}
</Draggable>
);
},
[isProcessing, technician, bodyshop, cardSettings, maxCardHeight, setMaxCardHeight, maxCardWidth, setMaxCardWidth]
);
const renderDroppable = useCallback(
(provided, renderedCards) => {
const Component = orientation === "vertical" ? VirtuosoGrid : Virtuoso;
const FinalComponent = collapsed ? "div" : Component;
const commonProps = {
data: renderedCards,
customScrollParent: laneRef.current
};
const verticalProps = {
...commonProps,
listClassName: "grid-container",
itemClassName: "grid-item",
components: {
List: ListComponent,
Item: ItemComponent
},
itemContent: (index, item) => <ItemWrapper>{renderDraggable(index, item)}</ItemWrapper>,
overscan: { main: 10, reverse: 10 },
// Ensure a minimum height for empty lanes to allow dropping
style: renderedCards.length === 0 ? { minHeight: "5px" } : {}
};
const horizontalProps = {
...commonProps,
components: { Item: HeightPreservingItem },
overscan: { main: 3, reverse: 3 },
itemContent: (index, item) => renderDraggable(index, item),
style: {
minWidth: maxCardWidth,
minHeight: maxLaneHeight
}
};
const componentProps = orientation === "vertical" ? verticalProps : horizontalProps;
const finalComponentProps = collapsed
? orientation === "horizontal"
? {
style: {
height: maxLaneHeight
}
}
: {}
: componentProps;
// Always render placeholder for empty lanes in vertical mode to ensure droppable area
const shouldRenderPlaceholder = orientation === "vertical" ? collapsed || renderedCards.length === 0 : collapsed;
return (
<HeightMemoryWrapper
itemKey={objectHash({
id,
orientation,
cardSettings,
cardLength: renderedCards?.length
})}
maxHeight={maxLaneHeight}
setMaxHeight={setMaxLaneHeight}
override={orientation !== "horizontal" && (collapsed || !renderedCards.length)}
>
const renderDraggable = (index, card) => {
if (!card) {
console.log("null card");
return null;
}
return (
<Draggable draggableId={card.id} index={index} key={card.id} isDragDisabled={isProcessing}>
{(provided, snapshot) => (
<div
ref={laneRef}
style={{ height: "100%", width: "100%" }}
className={`react-trello-lane ${collapsed ? "lane-collapsed" : ""}`}
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
style={provided.draggableProps.style}
className={`item ${snapshot.isDragging ? "is-dragging" : ""}`}
key={card.id}
>
<div {...provided.droppableProps} ref={provided.innerRef} style={{ ...provided.droppableProps.style }}>
<FinalComponent {...finalComponentProps} />
{shouldRenderPlaceholder && provided.placeholder}
</div>
</div>
</HeightMemoryWrapper>
);
},
[orientation, collapsed, renderDraggable, maxLaneHeight, setMaxLaneHeight, maxCardWidth, id, cardSettings]
);
const renderDragContainer = useCallback(
() => (
<Droppable
droppableId={id}
index={index}
type="lane"
direction={orientation === "horizontal" ? "vertical" : "grid"}
mode="virtual"
renderClone={(provided, snapshot, rubric) => {
const card = sortedCards[rubric.source.index];
return (
<div
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
style={{
...provided.draggableProps.style,
minHeight: maxCardHeight,
minWidth: maxCardWidth
}}
className={`clone ${snapshot.isDragging ? "is-dragging" : ""}`}
key={card.id}
<SizeMemoryWrapper
maxHeight={maxCardHeight}
setMaxHeight={setMaxCardHeight}
maxWidth={maxCardWidth}
setMaxWidth={setMaxCardWidth}
>
<ProductionBoardCard
technician={technician}
bodyshop={bodyshop}
cardSettings={cardSettings}
key={card.id}
className="react-trello-card"
card={card}
clone={false}
style={{ minHeight: maxCardHeight, minWidth: maxCardWidth }}
className="react-trello-card"
/>
</div>
);
}}
</SizeMemoryWrapper>
</div>
)}
</Draggable>
);
};
const renderDroppable = (provided, renderedCards) => {
const Component = orientation === "vertical" ? VirtuosoGrid : Virtuoso;
const FinalComponent = collapsed ? "div" : Component;
const commonProps = {
data: renderedCards,
customScrollParent: laneRef.current
};
const verticalProps = {
...commonProps,
listClassName: "grid-container",
itemClassName: "grid-item",
components: {
List: ListComponent,
Item: ItemComponent
},
itemContent: (index, item) => <ItemWrapper>{renderDraggable(index, item)}</ItemWrapper>,
overscan: { main: 10, reverse: 10 },
style: renderedCards.length === 0 ? { minHeight: "5px" } : {}
};
const horizontalProps = {
...commonProps,
components: { Item: HeightPreservingItem },
overscan: { main: 3, reverse: 3 },
itemContent: (index, item) => renderDraggable(index, item),
style: {
minWidth: maxCardWidth,
minHeight: maxLaneHeight
}
};
const componentProps = orientation === "vertical" ? verticalProps : horizontalProps;
const finalComponentProps = collapsed
? orientation === "horizontal"
? {
style: {
height: maxLaneHeight
}
}
: {}
: componentProps;
const shouldRenderPlaceholder = orientation === "vertical" ? collapsed || renderedCards.length === 0 : collapsed;
return (
<HeightMemoryWrapper
itemKey={objectHash({
id,
orientation,
cardSettings,
cardLength: renderedCards?.length
})}
maxHeight={maxLaneHeight}
setMaxHeight={setMaxLaneHeight}
override={orientation !== "horizontal" && (collapsed || !renderedCards.length)}
>
{(provided) => renderDroppable(provided, sortedCards)}
</Droppable>
),
[
id,
index,
orientation,
renderDroppable,
sortedCards,
technician,
bodyshop,
cardSettings,
maxCardHeight,
maxCardWidth
]
<div
ref={laneRef}
style={{ height: "100%", width: "100%" }}
className={`react-trello-lane ${collapsed ? "lane-collapsed" : ""}`}
>
<div {...provided.droppableProps} ref={provided.innerRef} style={{ ...provided.droppableProps.style }}>
<FinalComponent {...finalComponentProps} />
{shouldRenderPlaceholder && provided.placeholder}
</div>
</div>
</HeightMemoryWrapper>
);
};
const renderDragContainer = () => (
<Droppable
droppableId={id}
index={index}
type="lane"
direction={orientation === "horizontal" ? "vertical" : "grid"}
mode="virtual"
renderClone={(provided, snapshot, rubric) => {
const card = sortedCards[rubric.source.index];
return (
<div
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
style={{
...provided.draggableProps.style,
minHeight: maxCardHeight,
minWidth: maxCardWidth
}}
className={`clone ${snapshot.isDragging ? "is-dragging" : ""}`}
key={card.id}
>
<ProductionBoardCard
technician={technician}
bodyshop={bodyshop}
cardSettings={cardSettings}
key={card.id}
className="react-trello-card"
card={card}
clone={false}
/>
</div>
);
}}
>
{(provided) => renderDroppable(provided, sortedCards)}
</Droppable>
);
return (

View File

@@ -51,7 +51,9 @@ export default defineConfig(({ command, mode }) => {
const enableReactCompiler =
process.env.VITE_ENABLE_COMPILER_IN_DEV || (isBuild && (mode === "production" || isTestBuild));
console.log(enableReactCompiler ? "React Compiler enabled" : "React Compiler disabled");
logger.info(
enableReactCompiler ? chalk.green.bold("React Compiler enabled") : chalk.yellow.bold("React Compiler disabled")
);
return {
base: "/",
@@ -121,17 +123,7 @@ export default defineConfig(({ command, mode }) => {
enableReactCompiler
? {
babel: {
plugins: [
[
"babel-plugin-react-compiler",
{
// Exclude third-party drag-and-drop library from compilation
sources: (filename) => {
return !filename.includes("trello-board/dnd");
}
}
]
]
plugins: [["babel-plugin-react-compiler"]]
}
}
: undefined
@@ -221,7 +213,6 @@ export default defineConfig(({ command, mode }) => {
build: {
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {