IO-3431 Jobs Image Gallery
Signed-off-by: Allan Carr <allan@imexsystems.ca>
This commit is contained in:
@@ -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({
|
||||
)}
|
||||
<Col span={24}>
|
||||
<Card title={t("jobs.labels.documents-images")}>
|
||||
<Gallery
|
||||
<LocalMediaGrid
|
||||
images={galleryImages.images}
|
||||
onClick={(index) => {
|
||||
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}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Card title={t("jobs.labels.documents-other")}>
|
||||
<Gallery
|
||||
<LocalMediaGrid
|
||||
images={galleryImages.other}
|
||||
thumbnailStyle={() => {
|
||||
return {
|
||||
backgroundImage: <FileExcelFilled />,
|
||||
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}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
<div aria-label="media gallery unavailable" style={{ position: "relative", minHeight: 80 }}>
|
||||
<div style={messageStyle}>No job selected.</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="clearfix">
|
||||
<Gallery
|
||||
images={galleryImages}
|
||||
backdropClosesModal={true}
|
||||
onSelect={(index) => {
|
||||
setgalleryImages(galleryImages.map((g, idx) => (index === idx ? { ...g, isSelected: !g.isSelected } : g)));
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="clearfix"
|
||||
style={{ position: "relative", minHeight: 80 }}
|
||||
data-jobid={jobId}
|
||||
aria-label={`media gallery for job ${jobId}`}
|
||||
>
|
||||
{isLoading && galleryImages.length === 0 && (
|
||||
<div className="local-gallery-loading" style={messageStyle} role="status" aria-live="polite">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
)}
|
||||
{galleryImages.length > 0 && (
|
||||
<LocalMediaGrid images={galleryImages} minColumns={4} context={context} onToggle={handleToggle} />
|
||||
)}
|
||||
{galleryImages.length > 0 && (
|
||||
<div style={{ fontSize: 10, color: "#888", marginTop: 4 }} aria-live="off">
|
||||
{`${t("general.labels.media")}: ${galleryImages.length}`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user