diff --git a/client/src/components/error-boundary/error-boundary.component.jsx b/client/src/components/error-boundary/error-boundary.component.jsx
index c0d4b48b1..20962704a 100644
--- a/client/src/components/error-boundary/error-boundary.component.jsx
+++ b/client/src/components/error-boundary/error-boundary.component.jsx
@@ -38,7 +38,7 @@ class ErrorBoundary extends React.Component {
}
handleErrorSubmit = () => {
- window.$crisp.push([
+ window.$crisp?.push([
"do",
"message:send",
[
@@ -53,7 +53,7 @@ class ErrorBoundary extends React.Component {
]
]);
- window.$crisp.push(["do", "chat:open"]);
+ window.$crisp?.push(["do", "chat:open"]);
// const errorDescription = `**Please add relevant details about what you were doing before you encountered this issue**
@@ -78,7 +78,7 @@ class ErrorBoundary extends React.Component {
if (this.state.hasErrored === true) {
logImEXEvent("error_boundary_rendered", { error, info });
- window.$crisp.push([
+ window.$crisp?.push([
"set",
"session:event",
[
diff --git a/client/src/components/jobs-documents-gallery/jobs-documents-gallery.component.jsx b/client/src/components/jobs-documents-gallery/jobs-documents-gallery.component.jsx
index ab72147b4..a6cd07770 100644
--- a/client/src/components/jobs-documents-gallery/jobs-documents-gallery.component.jsx
+++ b/client/src/components/jobs-documents-gallery/jobs-documents-gallery.component.jsx
@@ -1,7 +1,7 @@
import { EditFilled, FileExcelFilled, SyncOutlined } from "@ant-design/icons";
import { Button, Card, Col, Row, Space } from "antd";
import { useEffect, useState } from "react";
-import { Gallery } from "react-grid-gallery";
+import { Gallery } from "../../vendor/react-grid-gallery";
import { useTranslation } from "react-i18next";
import Lightbox from "react-image-lightbox";
import "react-image-lightbox/style.css";
diff --git a/client/src/components/jobs-documents-gallery/jobs-documents-gallery.external.component.jsx b/client/src/components/jobs-documents-gallery/jobs-documents-gallery.external.component.jsx
index 34979d8ca..1d78349c8 100644
--- a/client/src/components/jobs-documents-gallery/jobs-documents-gallery.external.component.jsx
+++ b/client/src/components/jobs-documents-gallery/jobs-documents-gallery.external.component.jsx
@@ -1,5 +1,5 @@
import { useEffect } from "react";
-import { Gallery } from "react-grid-gallery";
+import { Gallery } from "../../vendor/react-grid-gallery";
import { useTranslation } from "react-i18next";
import { GenerateSrcUrl, GenerateThumbUrl } from "./job-documents.utility";
diff --git a/client/src/pages/csi/csi.container.page.jsx b/client/src/pages/csi/csi.container.page.jsx
index 975b7c53d..24eee7da3 100644
--- a/client/src/pages/csi/csi.container.page.jsx
+++ b/client/src/pages/csi/csi.container.page.jsx
@@ -34,7 +34,7 @@ export function CsiContainerPage({ currentUser }) {
const getAxiosData = useCallback(async () => {
try {
try {
- window.$crisp.push(["do", "chat:hide"]);
+ window.$crisp?.push(["do", "chat:hide"]);
} catch {
console.log("Unable to attach to crisp instance. ");
}
diff --git a/client/src/redux/user/user.sagas.js b/client/src/redux/user/user.sagas.js
index ce727c01e..0501e9918 100644
--- a/client/src/redux/user/user.sagas.js
+++ b/client/src/redux/user/user.sagas.js
@@ -238,7 +238,7 @@ export function* signInSuccessSaga({ payload }) {
LogRocket.identify(payload.email);
try {
- window.$crisp.push(["set", "user:nickname", [payload.displayName || payload.email]]);
+ window.$crisp?.push(["set", "user:nickname", [payload.displayName || payload.email]]);
InstanceRenderManager({
executeFunction: true,
@@ -269,9 +269,9 @@ export function* signInSuccessSaga({ payload }) {
]
: [])
];
- window.$crisp.push(["set", "session:segments", [segs]]);
+ window.$crisp?.push(["set", "session:segments", [segs]]);
if (isParts) {
- window.$crisp.push(["do", "chat:hide"]);
+ window.$crisp?.push(["do", "chat:hide"]);
}
} catch {
// no-op
@@ -359,9 +359,9 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
try {
//amplitude.setGroup('Shop', payload.shopname);
- window.$crisp.push(["set", "user:company", [payload.shopname]]);
+ window.$crisp?.push(["set", "user:company", [payload.shopname]]);
if (authRecord[0] && authRecord[0].user.validemail) {
- window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]);
+ window.$crisp?.push(["set", "user:email", [authRecord[0].user.email]]);
}
// Build consolidated Crisp segments including instance, region, features, and parts mode
@@ -402,10 +402,10 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
segments.push(InstanceRenderManager({ imex: "ImexPartsManagement", rome: "RomePartsManagement" }));
}
- window.$crisp.push(["set", "session:segments", [segments]]);
+ window.$crisp?.push(["set", "session:segments", [segments]]);
// Hide/show Crisp chat based on parts mode or features
- window.$crisp.push(["do", isParts ? "chat:hide" : "chat:show"]);
+ window.$crisp?.push(["do", isParts ? "chat:hide" : "chat:show"]);
InstanceRenderManager({
executeFunction: true,
diff --git a/client/src/vendor/react-grid-gallery/CheckButton.jsx b/client/src/vendor/react-grid-gallery/CheckButton.jsx
new file mode 100644
index 000000000..4b3e654cd
--- /dev/null
+++ b/client/src/vendor/react-grid-gallery/CheckButton.jsx
@@ -0,0 +1,62 @@
+import { useState } from "react";
+import * as styles from "./styles";
+
+export const CheckButton = ({
+ isSelected = false,
+ isVisible = true,
+ onClick,
+ color = "#FFFFFFB2",
+ selectedColor = "#4285F4FF",
+ hoverColor = "#FFFFFFFF",
+}) => {
+ const [hover, setHover] = useState(false);
+
+ const circleStyle = { display: isSelected ? "block" : "none" };
+ const fillColor = isSelected ? selectedColor : hover ? hoverColor : color;
+
+ const handleMouseOver = () => setHover(true);
+ const handleMouseOut = () => setHover(false);
+
+ return (
+
+ );
+};
diff --git a/client/src/vendor/react-grid-gallery/Gallery.jsx b/client/src/vendor/react-grid-gallery/Gallery.jsx
new file mode 100644
index 000000000..7992f8efa
--- /dev/null
+++ b/client/src/vendor/react-grid-gallery/Gallery.jsx
@@ -0,0 +1,63 @@
+import { Image } from "./Image";
+import { useContainerWidth } from "./useContainerWidth";
+import { buildLayoutFlat } from "./buildLayout";
+import * as styles from "./styles";
+
+export const Gallery = ({
+ images,
+ id = "ReactGridGallery",
+ enableImageSelection = true,
+ onSelect = () => {},
+ rowHeight = 180,
+ maxRows,
+ margin = 2,
+ defaultContainerWidth = 0,
+ onClick = () => {},
+ tileViewportStyle,
+ thumbnailStyle,
+ tagStyle,
+ thumbnailImageComponent
+}) => {
+ const { containerRef, containerWidth } = useContainerWidth(defaultContainerWidth);
+
+ const thumbnails = buildLayoutFlat(images, {
+ containerWidth,
+ maxRows,
+ rowHeight,
+ margin
+ });
+
+ const handleSelect = (index, event) => {
+ event.preventDefault();
+ onSelect(index, images[index], event);
+ };
+
+ const handleClick = (index, event) => {
+ onClick(index, images[index], event);
+ };
+
+ return (
+
+
+ {thumbnails.map((item, index) => (
+
+ ))}
+
+
+ );
+};
+
+Gallery.displayName = "Gallery";
diff --git a/client/src/vendor/react-grid-gallery/Image.jsx b/client/src/vendor/react-grid-gallery/Image.jsx
new file mode 100644
index 000000000..47ad61cb0
--- /dev/null
+++ b/client/src/vendor/react-grid-gallery/Image.jsx
@@ -0,0 +1,133 @@
+import { useState } from "react";
+import { CheckButton } from "./CheckButton";
+import * as styles from "./styles";
+import { getStyle } from "./styles";
+
+export const Image = ({
+ item,
+ thumbnailImageComponent: ThumbnailImageComponent,
+ isSelectable = true,
+ thumbnailStyle,
+ tagStyle,
+ tileViewportStyle,
+ margin,
+ index,
+ onSelect,
+ onClick,
+}) => {
+ const styleContext = { item };
+
+ const [hover, setHover] = useState(false);
+
+ const thumbnailProps = {
+ key: index,
+ "data-testid": "grid-gallery-item_thumbnail",
+ src: item.src,
+ alt: item.alt ? item.alt : "",
+ title: typeof item.caption === "string" ? item.caption : null,
+ style: getStyle(thumbnailStyle, styles.thumbnail, styleContext),
+ };
+
+ const handleCheckButtonClick = (event) => {
+ if (!isSelectable) {
+ return;
+ }
+ onSelect(index, event);
+ };
+
+ const handleViewportClick = (event) => {
+ onClick(index, event);
+ };
+
+ const thumbnailImageProps = {
+ item,
+ index,
+ margin,
+ onSelect,
+ onClick,
+ isSelectable,
+ tileViewportStyle,
+ thumbnailStyle,
+ tagStyle,
+ };
+
+ return (
+ setHover(true)}
+ onMouseLeave={() => setHover(false)}
+ style={styles.galleryItem({ margin })}
+ >
+
+
+
+
+ {!!item.tags && (
+
+ {item.tags.map((tag, index) => (
+
+
+ {tag.value}
+
+
+ ))}
+
+ )}
+
+ {!!item.customOverlay && (
+
+ {item.customOverlay}
+
+ )}
+
+
+
+
+ {ThumbnailImageComponent ? (
+
+ ) : (
+
![]()
+ )}
+
+ {item.thumbnailCaption && (
+
+ {item.thumbnailCaption}
+
+ )}
+
+ );
+};
diff --git a/client/src/vendor/react-grid-gallery/buildLayout.js b/client/src/vendor/react-grid-gallery/buildLayout.js
new file mode 100644
index 000000000..cc6f56bb5
--- /dev/null
+++ b/client/src/vendor/react-grid-gallery/buildLayout.js
@@ -0,0 +1,100 @@
+const calculateCutOff = (
+ items,
+ totalRowWidth,
+ protrudingWidth
+) => {
+ const cutOff = [];
+ let cutSum = 0;
+ for (let i in items) {
+ const item = items[i];
+ const fractionOfWidth = item.scaledWidth / totalRowWidth;
+ cutOff[i] = Math.floor(fractionOfWidth * protrudingWidth);
+ cutSum += cutOff[i];
+ }
+
+ let stillToCutOff = protrudingWidth - cutSum;
+ while (stillToCutOff > 0) {
+ for (let i in cutOff) {
+ cutOff[i]++;
+ stillToCutOff--;
+ if (stillToCutOff < 0) break;
+ }
+ }
+ return cutOff;
+};
+
+const getRow = (
+ images,
+ { containerWidth, rowHeight, margin }
+) => {
+ const row = [];
+ const imgMargin = 2 * margin;
+ const items = [...images];
+
+ let totalRowWidth = 0;
+ while (items.length > 0 && totalRowWidth < containerWidth) {
+ const item = items.shift();
+ const scaledWidth = Math.floor(rowHeight * (item.width / item.height));
+ const extendedItem = {
+ ...item,
+ scaledHeight: rowHeight,
+ scaledWidth,
+ viewportWidth: scaledWidth,
+ marginLeft: 0,
+ };
+ row.push(extendedItem);
+ totalRowWidth += extendedItem.scaledWidth + imgMargin;
+ }
+
+ const protrudingWidth = totalRowWidth - containerWidth;
+ if (row.length > 0 && protrudingWidth > 0) {
+ const cutoff = calculateCutOff(row, totalRowWidth, protrudingWidth);
+ for (const i in row) {
+ const pixelsToRemove = cutoff[i];
+ const item = row[i];
+ item.marginLeft = -Math.abs(Math.floor(pixelsToRemove / 2));
+ item.viewportWidth = item.scaledWidth - pixelsToRemove;
+ }
+ }
+
+ return [row, items];
+};
+
+const getRows = (
+ images,
+ options,
+ rows = []
+) => {
+ const [row, imagesLeft] = getRow(images, options);
+ const nextRows = [...rows, row];
+
+ if (options.maxRows && nextRows.length >= options.maxRows) {
+ return nextRows;
+ }
+ if (imagesLeft.length) {
+ return getRows(imagesLeft, options, nextRows);
+ }
+ return nextRows;
+};
+
+export const buildLayout = (
+ images,
+ { containerWidth, maxRows, rowHeight, margin }
+) => {
+ rowHeight = typeof rowHeight === "undefined" ? 180 : rowHeight;
+ margin = typeof margin === "undefined" ? 2 : margin;
+
+ if (!images) return [];
+ if (!containerWidth) return [];
+
+ const options = { containerWidth, maxRows, rowHeight, margin };
+ return getRows(images, options);
+};
+
+export const buildLayoutFlat = (
+ images,
+ options
+) => {
+ const rows = buildLayout(images, options);
+ return [].concat.apply([], rows);
+};
diff --git a/client/src/vendor/react-grid-gallery/index.js b/client/src/vendor/react-grid-gallery/index.js
new file mode 100644
index 000000000..196293106
--- /dev/null
+++ b/client/src/vendor/react-grid-gallery/index.js
@@ -0,0 +1,3 @@
+export { Gallery } from "./Gallery";
+export { CheckButton } from "./CheckButton";
+export { buildLayout, buildLayoutFlat } from "./buildLayout";
diff --git a/client/src/vendor/react-grid-gallery/styles.js b/client/src/vendor/react-grid-gallery/styles.js
new file mode 100644
index 000000000..62f93b703
--- /dev/null
+++ b/client/src/vendor/react-grid-gallery/styles.js
@@ -0,0 +1,185 @@
+export const getStyle = (
+ styleProp,
+ fallback,
+ context
+) => {
+ if (typeof styleProp === "function") {
+ return styleProp(context);
+ }
+ if (typeof styleProp === "object") {
+ return styleProp;
+ }
+ return fallback(context);
+};
+
+const rotationTransformMap = {
+ 3: "rotate(180deg)",
+ 2: "rotateY(180deg)",
+ 4: "rotate(180deg) rotateY(180deg)",
+ 5: "rotate(270deg) rotateY(180deg)",
+ 6: "rotate(90deg)",
+ 7: "rotate(90deg) rotateY(180deg)",
+ 8: "rotate(270deg)",
+};
+
+const SELECTION_MARGIN = 16;
+
+export const gallery = {
+ display: "flex",
+ flexWrap: "wrap",
+};
+
+export const thumbnail = ({ item }) => {
+ const rotationTransformValue = rotationTransformMap[item.orientation];
+
+ const style = {
+ cursor: "pointer",
+ maxWidth: "none",
+ width: item.scaledWidth,
+ height: item.scaledHeight,
+ marginLeft: item.marginLeft,
+ marginTop: 0,
+ transform: rotationTransformValue,
+ };
+
+ if (item.isSelected) {
+ const ratio = item.scaledWidth / item.scaledHeight;
+ const viewportHeight = item.scaledHeight - SELECTION_MARGIN * 2;
+ const viewportWidth = item.viewportWidth - SELECTION_MARGIN * 2;
+
+ let height, width;
+ if (item.scaledWidth > item.scaledHeight) {
+ width = item.scaledWidth - SELECTION_MARGIN * 2;
+ height = Math.floor(width / ratio);
+ } else {
+ height = item.scaledHeight - SELECTION_MARGIN * 2;
+ width = Math.floor(height * ratio);
+ }
+
+ const marginTop = Math.abs(Math.floor((viewportHeight - height) / 2));
+ const marginLeft = Math.abs(Math.floor((viewportWidth - width) / 2));
+
+ style.width = width;
+ style.height = height;
+ style.marginLeft = marginLeft === 0 ? 0 : -marginLeft;
+ style.marginTop = marginTop === 0 ? 0 : -marginTop;
+ }
+
+ return style;
+};
+
+export const tileViewport = ({
+ item,
+}) => {
+ const styles = {
+ width: item.viewportWidth,
+ height: item.scaledHeight,
+ overflow: "hidden",
+ };
+ if (item.nano) {
+ styles.background = `url(${item.nano})`;
+ styles.backgroundSize = "cover";
+ styles.backgroundPosition = "center center";
+ }
+ if (item.isSelected) {
+ styles.width = item.viewportWidth - SELECTION_MARGIN * 2;
+ styles.height = item.scaledHeight - SELECTION_MARGIN * 2;
+ styles.margin = SELECTION_MARGIN;
+ }
+ return styles;
+};
+
+export const customOverlay = ({
+ hover,
+}) => ({
+ pointerEvents: "none",
+ opacity: hover ? 1 : 0,
+ position: "absolute",
+ height: "100%",
+ width: "100%",
+});
+
+export const galleryItem = ({ margin }) => ({
+ margin,
+ WebkitUserSelect: "none",
+ position: "relative",
+ background: "#eee",
+ padding: "0px",
+});
+
+export const tileOverlay = ({
+ showOverlay,
+}) => ({
+ pointerEvents: "none",
+ opacity: 1,
+ position: "absolute",
+ height: "100%",
+ width: "100%",
+ background: showOverlay
+ ? "linear-gradient(to bottom,rgba(0,0,0,0.26),transparent 56px,transparent)"
+ : "none",
+});
+
+export const tileIconBar = {
+ pointerEvents: "none",
+ opacity: 1,
+ position: "absolute",
+ height: "36px",
+ width: "100%",
+};
+
+export const tileDescription = {
+ background: "white",
+ width: "100%",
+ margin: 0,
+ userSelect: "text",
+ WebkitUserSelect: "text",
+ MozUserSelect: "text",
+ overflow: "hidden",
+};
+
+export const bottomBar = {
+ padding: "2px",
+ pointerEvents: "none",
+ position: "absolute",
+ minHeight: "0px",
+ maxHeight: "160px",
+ width: "100%",
+ bottom: "0px",
+ overflow: "hidden",
+};
+
+export const tagItemBlock = {
+ display: "inline-block",
+ cursor: "pointer",
+ pointerEvents: "visible",
+ margin: "2px",
+};
+
+export const tagItem = () => ({
+ display: "inline",
+ padding: ".2em .6em .3em",
+ fontSize: "75%",
+ fontWeight: "600",
+ lineHeight: "1",
+ color: "yellow",
+ background: "rgba(0,0,0,0.65)",
+ textAlign: "center",
+ whiteSpace: "nowrap",
+ verticalAlign: "baseline",
+ borderRadius: ".25em",
+});
+
+export const checkButton = ({
+ isVisible,
+}) => ({
+ visibility: isVisible ? "visible" : "hidden",
+ background: "none",
+ float: "left",
+ width: 36,
+ height: 36,
+ border: "none",
+ padding: 6,
+ cursor: "pointer",
+ pointerEvents: "visible",
+});
diff --git a/client/src/vendor/react-grid-gallery/useContainerWidth.js b/client/src/vendor/react-grid-gallery/useContainerWidth.js
new file mode 100644
index 000000000..5464d51f4
--- /dev/null
+++ b/client/src/vendor/react-grid-gallery/useContainerWidth.js
@@ -0,0 +1,37 @@
+import { useCallback, useRef, useState } from "react";
+
+export function useContainerWidth(defaultContainerWidth) {
+ const ref = useRef(null);
+ const observerRef = useRef();
+
+ const [containerWidth, setContainerWidth] = useState(defaultContainerWidth);
+
+ const containerRef = useCallback((node) => {
+ observerRef.current?.disconnect();
+ observerRef.current = undefined;
+
+ ref.current = node;
+
+ const updateWidth = () => {
+ if (!ref.current) {
+ return;
+ }
+ let width = ref.current.clientWidth;
+ try {
+ width = ref.current.getBoundingClientRect().width;
+ } catch {
+ //
+ }
+ setContainerWidth(Math.floor(width));
+ };
+
+ updateWidth();
+
+ if (node && typeof ResizeObserver !== "undefined") {
+ observerRef.current = new ResizeObserver(updateWidth);
+ observerRef.current.observe(node);
+ }
+ }, []);
+
+ return { containerRef, containerWidth };
+}
diff --git a/docker-compose.yml b/docker-compose.yml
index 261076522..0662dd9bd 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -21,10 +21,11 @@ services:
- redis-node-1-data:/data
- redis-lock:/redis-lock
healthcheck:
- test: [ "CMD", "redis-cli", "ping" ]
+ test: [ "CMD", "sh", "-c", "redis-cli ping && redis-cli cluster info | grep cluster_state:ok" ]
interval: 10s
timeout: 5s
retries: 10
+ start_period: 15s
# Redis Node 2
redis-node-2:
@@ -39,10 +40,11 @@ services:
- redis-node-2-data:/data
- redis-lock:/redis-lock
healthcheck:
- test: [ "CMD", "redis-cli", "ping" ]
+ test: [ "CMD", "sh", "-c", "redis-cli ping && redis-cli cluster info | grep cluster_state:ok" ]
interval: 10s
timeout: 5s
retries: 10
+ start_period: 15s
# Redis Node 3
redis-node-3:
@@ -57,10 +59,11 @@ services:
- redis-node-3-data:/data
- redis-lock:/redis-lock
healthcheck:
- test: [ "CMD", "redis-cli", "ping" ]
+ test: [ "CMD", "sh", "-c", "redis-cli ping && redis-cli cluster info | grep cluster_state:ok" ]
interval: 10s
timeout: 5s
retries: 10
+ start_period: 15s
# LocalStack: Used to emulate AWS services locally, currently setup for SES
# Notes: Set the ENV Debug to 1 for additional logging
diff --git a/redis/entrypoint.sh b/redis/entrypoint.sh
index a5a527e30..caf1e8f8d 100644
--- a/redis/entrypoint.sh
+++ b/redis/entrypoint.sh
@@ -28,6 +28,10 @@ if [ ! -f "$LOCKFILE" ]; then
--cluster-replicas 0
echo "Redis Cluster initialized."
+
+ # Wait for cluster to be fully ready
+ echo "Waiting for cluster to be fully operational..."
+ sleep 3
else
echo "Cluster already initialized, skipping initialization."
fi
diff --git a/server.js b/server.js
index 25402623e..099ae3562 100644
--- a/server.js
+++ b/server.js
@@ -213,7 +213,15 @@ const connectToRedisCluster = async () => {
const clusterRetryStrategy = (times) => {
const delay =
Math.min(CLUSTER_RETRY_BASE_DELAY + times * 50, CLUSTER_RETRY_MAX_DELAY) + Math.random() * CLUSTER_RETRY_JITTER;
- logger.log(`Redis cluster not yet ready. Retrying in ${delay.toFixed(2)}ms`, "WARN", "redis", "api");
+ // Only log every 5th retry or after 10 attempts to reduce noise during startup
+ if (times % 5 === 0 || times > 10) {
+ logger.log(
+ `Redis cluster not yet ready. Retry attempt ${times}, waiting ${delay.toFixed(2)}ms`,
+ "WARN",
+ "redis",
+ "api"
+ );
+ }
return delay;
};