feature/IO-2924-Refactor-Production-Board-For-Sockets - Checkpoint
Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
@@ -3,7 +3,7 @@ import { useApolloClient } from "@apollo/client";
|
|||||||
import Board from "./trello-board/index";
|
import Board from "./trello-board/index";
|
||||||
import { Button, notification, Skeleton, Space } from "antd";
|
import { Button, notification, Skeleton, Space } from "antd";
|
||||||
import { PageHeader } from "@ant-design/pro-layout";
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
import React, { useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -23,6 +23,7 @@ import cloneDeep from "lodash/cloneDeep";
|
|||||||
import isEqual from "lodash/isEqual";
|
import isEqual from "lodash/isEqual";
|
||||||
import { defaultFilters, mergeWithDefaults } from "./settings/defaultKanbanSettings.js";
|
import { defaultFilters, mergeWithDefaults } from "./settings/defaultKanbanSettings.js";
|
||||||
import NoteUpsertModal from "../../components/note-upsert-modal/note-upsert-modal.container";
|
import NoteUpsertModal from "../../components/note-upsert-modal/note-upsert-modal.container";
|
||||||
|
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -45,10 +46,118 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [isMoving, setIsMoving] = useState(false);
|
const [isMoving, setIsMoving] = useState(false);
|
||||||
const [orientation, setOrientation] = useState("vertical");
|
const [orientation, setOrientation] = useState("vertical");
|
||||||
|
const { socket } = useContext(SocketContext); // Access socket from the context
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
|
|
||||||
|
// Update the lanes or cards based on job update event
|
||||||
|
const handleJobUpdated = useCallback(
|
||||||
|
(updatedJob) => {
|
||||||
|
setBoardLanes((prevBoardLanes) => {
|
||||||
|
const updatedLanes = cloneDeep(prevBoardLanes.lanes);
|
||||||
|
|
||||||
|
// Find the lane containing the card with the updated job ID
|
||||||
|
const sourceLane = updatedLanes.find((lane) => lane.cards.some((card) => card.id === updatedJob.id));
|
||||||
|
|
||||||
|
if (!sourceLane) {
|
||||||
|
console.error("Source lane not found for the updated job.");
|
||||||
|
return prevBoardLanes; // Return the previous state if no source lane is found
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the card in the source lane
|
||||||
|
const cardIndex = sourceLane.cards.findIndex((card) => card.id === updatedJob.id);
|
||||||
|
const currentCard = sourceLane.cards[cardIndex];
|
||||||
|
|
||||||
|
if (!currentCard) {
|
||||||
|
console.error("Card not found for the updated job.");
|
||||||
|
return prevBoardLanes; // Return the previous state if the card is not found
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate through the properties of updatedJob and update the corresponding values in currentCard.metadata
|
||||||
|
Object.keys(updatedJob).forEach((key) => {
|
||||||
|
// Ensure the field exists in currentCard.metadata before updating it
|
||||||
|
if (key in currentCard.metadata && currentCard.metadata[key] !== updatedJob[key]) {
|
||||||
|
console.log(`Updating ${key} from ${currentCard.metadata[key]} to ${updatedJob[key]}`);
|
||||||
|
currentCard.metadata[key] = updatedJob[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark that data has been changed if any field was updated
|
||||||
|
const isDataChanged = !isEqual(currentCard.metadata, updatedJob);
|
||||||
|
|
||||||
|
// Check if the lane (status) has changed
|
||||||
|
const isLaneChanged = updatedJob.status !== sourceLane.id;
|
||||||
|
|
||||||
|
// Case 1: Both data and lane have changed
|
||||||
|
if (isDataChanged && isLaneChanged) {
|
||||||
|
console.log("Case 1: Data and Lane Changed");
|
||||||
|
|
||||||
|
// Remove the card from the source lane
|
||||||
|
const [cardToMove] = sourceLane.cards.splice(cardIndex, 1);
|
||||||
|
|
||||||
|
// Find the target lane based on the new status
|
||||||
|
const targetLane = updatedLanes.find((lane) => lane.id === updatedJob.status);
|
||||||
|
if (targetLane) {
|
||||||
|
// Move the card to the target lane and update its data
|
||||||
|
targetLane.cards.push({ ...cardToMove, metadata: { ...currentCard.metadata } });
|
||||||
|
|
||||||
|
// Update the lane titles with the new card count
|
||||||
|
sourceLane.title = `${sourceLane.title.split(" ")[0]} (${sourceLane.cards.length})`;
|
||||||
|
targetLane.title = `${targetLane.title.split(" ")[0]} (${targetLane.cards.length})`;
|
||||||
|
} else {
|
||||||
|
console.error("Target lane not found for the updated job.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Case 2: Only data has changed
|
||||||
|
else if (isDataChanged && !isLaneChanged) {
|
||||||
|
console.log("Case 2: Only Data Changed");
|
||||||
|
|
||||||
|
// Update the card's metadata in place
|
||||||
|
sourceLane.cards[cardIndex] = { ...currentCard, metadata: { ...currentCard.metadata } };
|
||||||
|
|
||||||
|
// Force a shallow change in the source lane to trigger re-render
|
||||||
|
sourceLane.cards = [...sourceLane.cards];
|
||||||
|
}
|
||||||
|
// Case 3: Only the lane has changed
|
||||||
|
else if (!isDataChanged && isLaneChanged) {
|
||||||
|
console.log("Case 3: Only Lane Changed");
|
||||||
|
|
||||||
|
// Remove the card from the source lane
|
||||||
|
const [cardToMove] = sourceLane.cards.splice(cardIndex, 1);
|
||||||
|
|
||||||
|
// Find the target lane based on the new status
|
||||||
|
const targetLane = updatedLanes.find((lane) => lane.id === updatedJob.status);
|
||||||
|
if (targetLane) {
|
||||||
|
// Move the card to the new lane without changing its data
|
||||||
|
targetLane.cards.push(cardToMove);
|
||||||
|
|
||||||
|
// Update the lane titles with the new card count
|
||||||
|
sourceLane.title = `${sourceLane.title.split(" ")[0]} (${sourceLane.cards.length})`;
|
||||||
|
targetLane.title = `${targetLane.title.split(" ")[0]} (${targetLane.cards.length})`;
|
||||||
|
} else {
|
||||||
|
console.error("Target lane not found for the updated job.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the updated lanes
|
||||||
|
return { lanes: updatedLanes };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setBoardLanes]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Listen for the job-updated event from the socket
|
||||||
|
if (socket) {
|
||||||
|
socket.on("job-updated", handleJobUpdated);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.off("job-updated", handleJobUpdated);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [socket, handleJobUpdated]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (associationSettings) {
|
if (associationSettings) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser }) {
|
|||||||
onError: (error) => console.error(`Error fetching jobs in production: ${error.message}`)
|
onError: (error) => console.error(`Error fetching jobs in production: ${error.message}`)
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION, {
|
// const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION, {
|
||||||
onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`)
|
// onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`)
|
||||||
});
|
// });
|
||||||
|
|
||||||
const { loading: associationSettingsLoading, data: associationSettings } = useQuery(QUERY_KANBAN_SETTINGS, {
|
const { loading: associationSettingsLoading, data: associationSettings } = useQuery(QUERY_KANBAN_SETTINGS, {
|
||||||
variables: { email: currentUser.email },
|
variables: { email: currentUser.email },
|
||||||
@@ -40,11 +40,11 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser }) {
|
|||||||
// This provides us the current version of the Lanes from the Redux store
|
// This provides us the current version of the Lanes from the Redux store
|
||||||
// const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {}));
|
// const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {}));
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
if (updatedJobs && data) {
|
// if (updatedJobs && data) {
|
||||||
refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`));
|
// refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`));
|
||||||
}
|
// }
|
||||||
}, [updatedJobs, data, refetch]);
|
// }, [updatedJobs, data, refetch]);
|
||||||
|
|
||||||
const filteredAssociationSettings = useMemo(() => {
|
const filteredAssociationSettings = useMemo(() => {
|
||||||
return associationSettings?.associations[0] || null;
|
return associationSettings?.associations[0] || null;
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ const useSocket = (bodyshop) => {
|
|||||||
const [clientId, setClientId] = useState(null); // State to store unique identifier
|
const [clientId, setClientId] = useState(null); // State to store unique identifier
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const handleBodyshopMessage = (message) => {
|
||||||
|
console.log(`Received message for bodyshop ${bodyshop.id}:`, message);
|
||||||
|
};
|
||||||
|
|
||||||
if (bodyshop && bodyshop.id) {
|
if (bodyshop && bodyshop.id) {
|
||||||
const endpoint = import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "https://localhost:3000";
|
const endpoint = import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "https://localhost:3000";
|
||||||
|
|
||||||
@@ -27,6 +31,11 @@ const useSocket = (bodyshop) => {
|
|||||||
socketInstance.on("connect", () => {
|
socketInstance.on("connect", () => {
|
||||||
console.log("Socket connected:", socketInstance.id);
|
console.log("Socket connected:", socketInstance.id);
|
||||||
setClientId(socketInstance.id);
|
setClientId(socketInstance.id);
|
||||||
|
|
||||||
|
if (bodyshop.id) {
|
||||||
|
socketInstance.on("bodyshop-message", handleBodyshopMessage);
|
||||||
|
socketInstance.emit("join-bodyshop-room", bodyshop.id);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socketInstance.on("reconnect", (attempt) => {
|
socketInstance.on("reconnect", (attempt) => {
|
||||||
@@ -41,6 +50,14 @@ const useSocket = (bodyshop) => {
|
|||||||
console.log("Socket disconnected");
|
console.log("Socket disconnected");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (bodyshop?.id) {
|
||||||
|
return () => {
|
||||||
|
socketInstance.emit("leave-bodyshop-room", bodyshop.id);
|
||||||
|
socketInstance.off("bodyshop-message", handleBodyshopMessage);
|
||||||
|
socketInstance.disconnect();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socketInstance.disconnect();
|
socketInstance.disconnect();
|
||||||
};
|
};
|
||||||
@@ -52,3 +69,24 @@ const useSocket = (bodyshop) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default useSocket;
|
export default useSocket;
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (socket && bodyshop && bodyshop.id) {
|
||||||
|
// const handleConnect = () => {
|
||||||
|
// socket.emit("join-bodyshop-room", bodyshop.id);
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// const handleBodyshopMessage = (message) => {
|
||||||
|
// console.log(`Received message for bodyshop ${bodyshop.id}:`, message);
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// socket.on("connect", handleConnect);
|
||||||
|
// socket.on("bodyshop-message", handleBodyshopMessage);
|
||||||
|
//
|
||||||
|
// return () => {
|
||||||
|
// socket.emit("leave-bodyshop-room", bodyshop.id);
|
||||||
|
// socket.off("connect", handleConnect);
|
||||||
|
// socket.off("bodyshop-message", handleBodyshopMessage);
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// }, [socket, bodyshop]);
|
||||||
|
|||||||
@@ -120,27 +120,6 @@ export function Manage({ conflict, bodyshop }) {
|
|||||||
});
|
});
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (socket && bodyshop && bodyshop.id) {
|
|
||||||
const handleConnect = () => {
|
|
||||||
socket.emit("join-bodyshop-room", bodyshop.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBodyshopMessage = (message) => {
|
|
||||||
console.log(`Received message for bodyshop ${bodyshop.id}:`, message);
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.on("connect", handleConnect);
|
|
||||||
socket.on("bodyshop-message", handleBodyshopMessage);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
socket.emit("leave-bodyshop-room", bodyshop.id);
|
|
||||||
socket.off("connect", handleConnect);
|
|
||||||
socket.off("bodyshop-message", handleBodyshopMessage);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [socket, bodyshop]);
|
|
||||||
|
|
||||||
const AppRouteTable = (
|
const AppRouteTable = (
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={
|
fallback={
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ const { isObject } = require("lodash");
|
|||||||
|
|
||||||
const jobUpdated = async (req, res) => {
|
const jobUpdated = async (req, res) => {
|
||||||
const { io, logger } = req;
|
const { io, logger } = req;
|
||||||
|
|
||||||
|
logger.log("job-update", "INFO", req.user?.email, null, {
|
||||||
|
message: `Job updated event received from Hasura`,
|
||||||
|
body: req?.body
|
||||||
|
});
|
||||||
|
|
||||||
if (!req?.body?.event?.data?.new || !isObject(req?.body?.event?.data?.new)) {
|
if (!req?.body?.event?.data?.new || !isObject(req?.body?.event?.data?.new)) {
|
||||||
logger.log("job-update-error", "ERROR", req.user?.email, null, {
|
logger.log("job-update-error", "ERROR", req.user?.email, null, {
|
||||||
message: `Malformed Job Update request sent from Hasura`,
|
message: `Malformed Job Update request sent from Hasura`,
|
||||||
@@ -13,9 +19,14 @@ const jobUpdated = async (req, res) => {
|
|||||||
message: `Malformed Job Update request sent from Hasura`
|
message: `Malformed Job Update request sent from Hasura`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const bodyshopID = req.body.event.data.new.shopid;
|
|
||||||
|
|
||||||
return res.json({ message: "Job updated" });
|
const updatedJob = req.body.event.data.new;
|
||||||
|
const bodyshopID = updatedJob.shopid;
|
||||||
|
|
||||||
|
// Emit the job-updated event only to the room corresponding to the bodyshop
|
||||||
|
io.to(bodyshopID).emit("job-updated", updatedJob);
|
||||||
|
|
||||||
|
return res.json({ message: "Job updated and event emitted" });
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = jobUpdated;
|
module.exports = jobUpdated;
|
||||||
|
|||||||
Reference in New Issue
Block a user