diff --git a/client/src/components/jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.component.jsx b/client/src/components/jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.component.jsx
index 1c14eb486..f2f5fbfd5 100644
--- a/client/src/components/jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.component.jsx
+++ b/client/src/components/jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.component.jsx
@@ -1,10 +1,9 @@
-import { EditFilled, FileExcelFilled, SyncOutlined } from "@ant-design/icons";
+import { EditFilled, SyncOutlined } from "@ant-design/icons";
import { Button, Card, Col, Row, Space } from "antd";
import axios from "axios";
import i18n from "i18next";
import { isFunction } from "lodash";
import { useCallback, useEffect, useState } from "react";
-import { Gallery } from "react-grid-gallery";
import { useTranslation } from "react-i18next";
import Lightbox from "react-image-lightbox";
import "react-image-lightbox/style.css";
@@ -18,19 +17,13 @@ import JobsDocumentsDownloadButton from "./jobs-document-imgproxy-gallery.downlo
import JobsDocumentsGalleryReassign from "./jobs-document-imgproxy-gallery.reassign.component";
import JobsDocumentsDeleteButton from "./jobs-documents-imgproxy-gallery.delete.component";
import JobsDocumentsGallerySelectAllComponent from "./jobs-documents-imgproxy-gallery.selectall.component";
+import LocalMediaGrid from "../jobs-documents-local-gallery/local-media-grid.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = () => ({});
-/*
-################################################################################################
- Developer Note:
- Known Technical Debt Item
- Modifications to this code requires complementary changes to the Cloudinary code. Cloudinary code will be removed upon completed migration.
-################################################################################################
-*/
function JobsDocumentsImgproxyComponent({
bodyshop,
data,
@@ -119,17 +112,12 @@ function JobsDocumentsImgproxyComponent({
)}
- {
setModalState({ open: true, index: index });
- // window.open(
- // item.fullsize,
- // "_blank",
- // "toolbar=0,location=0,menubar=0"
- // );
}}
- onSelect={(index) => {
+ onToggle={(index) => {
setGalleryImages({
...galleryImages,
images: galleryImages.images.map((g, idx) =>
@@ -137,30 +125,26 @@ function JobsDocumentsImgproxyComponent({
)
});
}}
+ minColumns={4}
+ expandHeight={true}
/>
- {
- return {
- backgroundImage: ,
- height: "100%",
- width: "100%",
- cursor: "pointer"
- };
- }}
onClick={(index) => {
window.open(galleryImages.other[index].source, "_blank", "toolbar=0,location=0,menubar=0");
}}
- onSelect={(index) => {
+ onToggle={(index) => {
setGalleryImages({
...galleryImages,
other: galleryImages.other.map((g, idx) => (index === idx ? { ...g, isSelected: !g.isSelected } : g))
});
}}
+ minColumns={4}
+ expandHeight={true}
/>
@@ -221,6 +205,7 @@ export const fetchImgproxyThumbnails = async ({ setStateCallback, jobId, billId,
width: 225,
isSelected: false,
key: value.key,
+ filename: value.key,
extension: value.extension,
id: value.id,
type: value.type,
@@ -259,6 +244,7 @@ export const fetchImgproxyThumbnails = async ({ setStateCallback, jobId, billId,
isSelected: false,
extension: value.extension,
key: value.key,
+ filename: value.key,
id: value.id,
type: value.type,
size: value.size
diff --git a/client/src/components/jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.external.component.jsx b/client/src/components/jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.external.component.jsx
index 0458d3aac..884ac5ac3 100644
--- a/client/src/components/jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.external.component.jsx
+++ b/client/src/components/jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.external.component.jsx
@@ -1,31 +1,112 @@
-import { useEffect } from "react";
-import { Gallery } from "react-grid-gallery";
-import { fetchImgproxyThumbnails } from "./jobs-documents-imgproxy-gallery.component";
+import { useEffect, useMemo, useState, useCallback } from "react";
+import axios from "axios";
+import { useTranslation } from "react-i18next";
+import LocalMediaGrid from "../jobs-documents-local-gallery/local-media-grid.component";
+import LoadingSpinner from "../loading-spinner/loading-spinner.component";
-/*
-################################################################################################
- Developer Note:
- Known Technical Debt Item
- Modifications to this code requires complementary changes to the Cloudinary code. Cloudinary code will be removed upon completed migration.
-################################################################################################
-*/
-
-function JobsDocumentImgproxyGalleryExternal({ jobId, externalMediaState }) {
+function JobsDocumentImgproxyGalleryExternal({ jobId, externalMediaState, context = "chat" }) {
const [galleryImages, setgalleryImages] = externalMediaState;
+ const [rawMedia, setRawMedia] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const { t } = useTranslation();
+
+ const fetchThumbnails = useCallback(async () => {
+ const result = await axios.post("/media/imgproxy/thumbnails", { jobid: jobId });
+ return result.data;
+ }, [jobId]);
useEffect(() => {
- if (jobId) fetchImgproxyThumbnails({ setStateCallback: setgalleryImages, jobId, imagesOnly: true });
- }, [jobId, setgalleryImages]);
+ if (!jobId) return;
+ setIsLoading(true);
+ fetchThumbnails()
+ .then(setRawMedia)
+ .catch(console.error)
+ .finally(() => setIsLoading(false));
+ }, [jobId, fetchThumbnails]);
+
+ const documents = useMemo(() => {
+ return rawMedia
+ .filter((v) => v.type?.startsWith("image"))
+ .map((v) => ({
+ src: v.thumbnailUrl,
+ thumbnail: v.thumbnailUrl,
+ fullsize: v.originalUrl,
+ width: 225,
+ height: 225,
+ thumbnailWidth: 225,
+ thumbnailHeight: 225,
+ caption: v.key,
+ filename: v.key,
+ // additional properties if needed
+ key: v.key,
+ id: v.id,
+ type: v.type,
+ size: v.size,
+ extension: v.extension
+ }));
+ }, [rawMedia]);
+
+ useEffect(() => {
+ const prevSelection = new Map(galleryImages.map((p) => [p.filename, p.isSelected]));
+ const nextImages = documents.map((d) => ({ ...d, isSelected: prevSelection.get(d.filename) || false }));
+ // Micro-optimization: if array length and each filename + selection flag match, skip creating a new array.
+ if (galleryImages.length === nextImages.length) {
+ let identical = true;
+ for (let i = 0; i < nextImages.length; i++) {
+ if (
+ galleryImages[i].filename !== nextImages[i].filename ||
+ galleryImages[i].isSelected !== nextImages[i].isSelected
+ ) {
+ identical = false;
+ break;
+ }
+ }
+ if (identical) {
+ setIsLoading(false); // ensure loading stops even on no-change
+ return;
+ }
+ }
+ setgalleryImages(nextImages);
+ setIsLoading(false); // stop loading after transform regardless of emptiness
+ }, [documents, setgalleryImages, galleryImages, jobId]);
+
+ const handleToggle = useCallback(
+ (idx) => {
+ setgalleryImages((imgs) => imgs.map((g, gIdx) => (gIdx === idx ? { ...g, isSelected: !g.isSelected } : g)));
+ },
+ [setgalleryImages]
+ );
+
+ const messageStyle = { textAlign: "center", padding: "1rem" };
+
+ if (!jobId) {
+ return (
+
+ );
+ }
return (
-
-
{
- setgalleryImages(galleryImages.map((g, idx) => (index === idx ? { ...g, isSelected: !g.isSelected } : g)));
- }}
- />
+
+ {isLoading && galleryImages.length === 0 && (
+
+
+
+ )}
+ {galleryImages.length > 0 && (
+
+ )}
+ {galleryImages.length > 0 && (
+
+ {`${t("general.labels.media")}: ${galleryImages.length}`}
+
+ )}
);
}
diff --git a/client/src/components/jobs-documents-local-gallery/jobs-documents-local-gallery.container.jsx b/client/src/components/jobs-documents-local-gallery/jobs-documents-local-gallery.container.jsx
index da94678a8..4ef9cb881 100644
--- a/client/src/components/jobs-documents-local-gallery/jobs-documents-local-gallery.container.jsx
+++ b/client/src/components/jobs-documents-local-gallery/jobs-documents-local-gallery.container.jsx
@@ -1,7 +1,6 @@
-import { EditFilled, FileExcelFilled, SyncOutlined } from "@ant-design/icons";
+import { EditFilled, SyncOutlined } from "@ant-design/icons";
import { Alert, Button, Card, Col, Row, Space } from "antd";
import { useEffect, useState } from "react";
-import { Gallery } from "react-grid-gallery";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -14,6 +13,7 @@ import JobsDocumentsLocalDeleteButton from "./jobs-documents-local-gallery.delet
import JobsLocalGalleryDownloadButton from "./jobs-documents-local-gallery.download";
import JobsDocumentsLocalGalleryReassign from "./jobs-documents-local-gallery.reassign.component";
import JobsDocumentsLocalGallerySelectAllComponent from "./jobs-documents-local-gallery.selectall.component";
+import LocalMediaGrid from "./local-media-grid.component";
import Lightbox from "react-image-lightbox";
import "react-image-lightbox/style.css";
@@ -132,54 +132,34 @@ export function JobsDocumentsLocalGallery({
-
+ )}
+ {
- toggleMediaSelected({ jobid: job.id, filename: image.filename });
- }}
- {...(optimized && {
- customControls: [
-
- ]
- })}
onClick={(index) => {
setModalState({ open: true, index: index });
- // const media = allMedia[job.id].find(
- // (m) => m.optimized === item.src
- // );
-
- // window.open(
- // media ? media.fullsize : item.fullsize,
- // "_blank",
- // "toolbar=0,location=0,menubar=0"
- // );
}}
+ onToggle={(index) => {
+ toggleMediaSelected({ jobid: job.id, filename: jobMedia.images[index].filename });
+ }}
+ minColumns={4}
+ expandHeight={true}
/>
- {
- return {
- backgroundImage: ,
- height: "100%",
- width: "100%",
- cursor: "pointer"
- };
- }}
onClick={(index) => {
window.open(jobMedia.other[index].fullsize, "_blank", "toolbar=0,location=0,menubar=0");
}}
- onSelect={(index, image) => {
- toggleMediaSelected({ jobid: job.id, filename: image.filename });
+ onToggle={(index) => {
+ toggleMediaSelected({ jobid: job.id, filename: jobMedia.other[index].filename });
}}
+ minColumns={4}
+ expandHeight={true}
/>
diff --git a/client/src/components/jobs-documents-local-gallery/local-media-grid.component.jsx b/client/src/components/jobs-documents-local-gallery/local-media-grid.component.jsx
index 277351296..f4c671f4f 100644
--- a/client/src/components/jobs-documents-local-gallery/local-media-grid.component.jsx
+++ b/client/src/components/jobs-documents-local-gallery/local-media-grid.component.jsx
@@ -6,15 +6,18 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
* Props:
* - images: Array<{ src, fullsize, filename?, isSelected? }>
* - onToggle(index)
+ * - onClick(index) optional for viewing
*/
export function LocalMediaGrid({
images,
onToggle,
+ onClick,
thumbSize = 100,
gap = 8,
minColumns = 3,
maxColumns = 12,
- context = "default"
+ context = "default",
+ expandHeight = false
}) {
const containerRef = useRef(null);
const [cols, setCols] = useState(() => {
@@ -114,8 +117,7 @@ export function LocalMediaGrid({
display: "grid",
gridTemplateColumns,
gap,
- maxHeight: 420,
- overflowY: "auto",
+ ...(expandHeight ? {} : { maxHeight: 420, overflowY: "auto" }),
overflowX: "hidden",
padding: 4,
justifyContent: justifyMode,
@@ -131,7 +133,7 @@ export function LocalMediaGrid({
role="listitem"
tabIndex={0}
aria-label={img.filename || `image ${idx + 1}`}
- onClick={() => onToggle(idx)}
+ onClick={() => onClick ? onClick(idx) : onToggle(idx)}
onKeyDown={(e) => handleKeyDown(e, idx)}
style={{
position: "relative",
@@ -197,6 +199,23 @@ export function LocalMediaGrid({
}}
/>
)}
+ {onClick && (
+ {
+ e.stopPropagation();
+ onToggle(idx);
+ }}
+ onClick={(e) => e.stopPropagation()}
+ style={{
+ position: 'absolute',
+ top: 5,
+ right: 5,
+ zIndex: 2
+ }}
+ />
+ )}
))}
{/* No placeholders needed; layout uses auto-fit for non-chat or fixed columns for chat */}