feature/IO-3499-React-19: Move react-grid-gallery to a vendor directory internally, will be removed shortly but for now we keep it

This commit is contained in:
Dave
2026-01-13 17:17:28 -05:00
parent 1764397195
commit 7bdfbfabe9
15 changed files with 615 additions and 17 deletions

View File

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

View File

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

View File

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

View File

@@ -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. ");
}

View File

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

View File

@@ -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 (
<div
data-testid="grid-gallery-item_check-button"
title="Select"
style={styles.checkButton({ isVisible })}
onClick={onClick}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
>
<svg
fill={fillColor}
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<radialGradient
id="shadow"
cx="38"
cy="95.488"
r="10.488"
gradientTransform="matrix(1 0 0 -1 -26 109)"
gradientUnits="userSpaceOnUse"
>
<stop offset=".832" stopColor="#010101"></stop>
<stop offset="1" stopColor="#010101" stopOpacity="0"></stop>
</radialGradient>
<circle
style={circleStyle}
opacity=".26"
fill="url(#shadow)"
cx="12"
cy="13.512"
r="10.488"
/>
<circle style={circleStyle} fill="#FFF" cx="12" cy="12.2" r="8.292" />
<path d="M0 0h24v24H0z" fill="none" />
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
</svg>
</div>
);
};

View File

@@ -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 (
<div id={id} className="ReactGridGallery" ref={containerRef}>
<div style={styles.gallery}>
{thumbnails.map((item, index) => (
<Image
key={item.key || index}
item={item}
index={index}
margin={margin}
height={rowHeight}
isSelectable={enableImageSelection}
onClick={handleClick}
onSelect={handleSelect}
tagStyle={tagStyle}
tileViewportStyle={tileViewportStyle}
thumbnailStyle={thumbnailStyle}
thumbnailImageComponent={thumbnailImageComponent}
/>
))}
</div>
</div>
);
};
Gallery.displayName = "Gallery";

View File

@@ -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 (
<div
className="ReactGridGallery_tile"
data-testid="grid-gallery-item"
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
style={styles.galleryItem({ margin })}
>
<div
className="ReactGridGallery_tile-icon-bar"
style={styles.tileIconBar}
>
<CheckButton
isSelected={item.isSelected}
isVisible={item.isSelected || (isSelectable && hover)}
onClick={handleCheckButtonClick}
/>
</div>
{!!item.tags && (
<div
className="ReactGridGallery_tile-bottom-bar"
style={styles.bottomBar}
>
{item.tags.map((tag, index) => (
<div
key={tag.key || index}
title={tag.title}
style={styles.tagItemBlock}
>
<span style={getStyle(tagStyle, styles.tagItem, styleContext)}>
{tag.value}
</span>
</div>
))}
</div>
)}
{!!item.customOverlay && (
<div
className="ReactGridGallery_custom-overlay"
style={styles.customOverlay({ hover })}
>
{item.customOverlay}
</div>
)}
<div
className="ReactGridGallery_tile-overlay"
style={styles.tileOverlay({
showOverlay: hover && !item.isSelected && isSelectable,
})}
/>
<div
className="ReactGridGallery_tile-viewport"
data-testid="grid-gallery-item_viewport"
style={getStyle(tileViewportStyle, styles.tileViewport, styleContext)}
onClick={handleViewportClick}
>
{ThumbnailImageComponent ? (
<ThumbnailImageComponent
{...thumbnailImageProps}
imageProps={thumbnailProps}
/>
) : (
<img {...thumbnailProps} />
)}
</div>
{item.thumbnailCaption && (
<div
className="ReactGridGallery_tile-description"
style={styles.tileDescription}
>
{item.thumbnailCaption}
</div>
)}
</div>
);
};

View File

@@ -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);
};

View File

@@ -0,0 +1,3 @@
export { Gallery } from "./Gallery";
export { CheckButton } from "./CheckButton";
export { buildLayout, buildLayoutFlat } from "./buildLayout";

View File

@@ -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",
});

View File

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

View File

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

View File

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

View File

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