Compare commits
5 Commits
revert-pr-
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d90acf4b89 | ||
|
|
68dd7f33ab | ||
|
|
9b62633ba6 | ||
|
|
021bf714d6 | ||
|
|
4a22aeca46 |
@@ -1,13 +1,21 @@
|
||||
import { Carousel } from "antd";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import { GenerateThumbUrl } from "../jobs-documents-gallery/job-documents.utility";
|
||||
import { fetchImgproxyThumbnails } from "../jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.component";
|
||||
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
||||
import CardTemplate from "./job-detail-cards.template.component";
|
||||
|
||||
export default function JobDetailCardsDocumentsComponent({ loading, data, bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
const hasMediaAccess = HasFeatureAccess({ bodyshop, featureName: "media" });
|
||||
const [thumbnails, setThumbnails] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.id) {
|
||||
fetchImgproxyThumbnails({ setStateCallback: setThumbnails, jobId: data.id, imagesOnly: true });
|
||||
}
|
||||
}, [data?.id]);
|
||||
|
||||
if (!data)
|
||||
return (
|
||||
@@ -22,18 +30,19 @@ export default function JobDetailCardsDocumentsComponent({ loading, data, bodysh
|
||||
title={t("jobs.labels.cards.documents")}
|
||||
extraLink={`/manage/jobs/${data.id}?tab=documents`}
|
||||
>
|
||||
{!hasMediaAccess && (
|
||||
<UpsellComponent disableMask upsell={upsellEnum().media.general}>
|
||||
{data.documents.length > 0 ? (
|
||||
{!hasMediaAccess && <UpsellComponent disableMask upsell={upsellEnum().media.general} />}
|
||||
{hasMediaAccess && (
|
||||
<>
|
||||
{thumbnails.length > 0 ? (
|
||||
<Carousel autoplay>
|
||||
{data.documents.map((item) => (
|
||||
<img key={item.id} src={GenerateThumbUrl(item)} alt={item.name} />
|
||||
{thumbnails.map((item) => (
|
||||
<img key={item.id} src={item.src} alt={item.filename} />
|
||||
))}
|
||||
</Carousel>
|
||||
) : (
|
||||
<div>{t("documents.errors.nodocuments")}</div>
|
||||
)}
|
||||
</UpsellComponent>
|
||||
</>
|
||||
)}
|
||||
</CardTemplate>
|
||||
);
|
||||
|
||||
@@ -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,61 @@
|
||||
import { useEffect } from "react";
|
||||
import { Gallery } from "react-grid-gallery";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import LocalMediaGrid from "../jobs-documents-local-gallery/local-media-grid.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import { fetchImgproxyThumbnails } from "./jobs-documents-imgproxy-gallery.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 [isLoading, setIsLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (jobId) fetchImgproxyThumbnails({ setStateCallback: setgalleryImages, jobId, imagesOnly: true });
|
||||
const fetchThumbnails = useCallback(async () => {
|
||||
await fetchImgproxyThumbnails({ setStateCallback: setgalleryImages, jobId, imagesOnly: true });
|
||||
}, [jobId, setgalleryImages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!jobId) return;
|
||||
setIsLoading(true);
|
||||
fetchThumbnails().finally(() => setIsLoading(false));
|
||||
}, [jobId, fetchThumbnails]);
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -67,6 +67,7 @@ export function JobsDocumentsLocalGallery({
|
||||
src: val.thumbnail,
|
||||
height: val.thumbnailHeight,
|
||||
width: val.thumbnailWidth,
|
||||
tags: [{ value: val.filename, title: val.filename }],
|
||||
...(val.optimized && { src: val.optimized, fullsize: val.src })
|
||||
});
|
||||
if (val.optimized) optimized = true;
|
||||
@@ -132,54 +133,34 @@ export function JobsDocumentsLocalGallery({
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Card title={t("jobs.labels.documents-images")}>
|
||||
<Gallery
|
||||
{optimized && (
|
||||
<Alert style={{ margin: "4px" }} message={t("documents.labels.optimizedimage")} type="success" />
|
||||
)}
|
||||
<LocalMediaGrid
|
||||
images={jobMedia.images}
|
||||
onSelect={(index, image) => {
|
||||
toggleMediaSelected({ jobid: job.id, filename: image.filename });
|
||||
}}
|
||||
{...(optimized && {
|
||||
customControls: [
|
||||
<Alert
|
||||
key="optimizedImageALert"
|
||||
style={{ margin: "4px" }}
|
||||
message={t("documents.labels.optimizedimage")}
|
||||
type="success"
|
||||
/>
|
||||
]
|
||||
})}
|
||||
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}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Card title={t("jobs.labels.documents-other")}>
|
||||
<Gallery
|
||||
<LocalMediaGrid
|
||||
images={jobMedia.other}
|
||||
thumbnailStyle={() => {
|
||||
return {
|
||||
backgroundImage: <FileExcelFilled />,
|
||||
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}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Checkbox } from "antd";
|
||||
import { Tag } from "antd";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
/**
|
||||
@@ -6,16 +8,20 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
* Props:
|
||||
* - images: Array<{ src, fullsize, filename?, isSelected? }>
|
||||
* - onToggle(index)
|
||||
* - onClick(index) optional for viewing
|
||||
* - thumbSize: automatically set to 125 for chat, 250 for default
|
||||
*/
|
||||
export function LocalMediaGrid({
|
||||
images,
|
||||
onToggle,
|
||||
thumbSize = 100,
|
||||
onClick,
|
||||
gap = 8,
|
||||
minColumns = 3,
|
||||
maxColumns = 12,
|
||||
context = "default"
|
||||
context = "default",
|
||||
expandHeight = false
|
||||
}) {
|
||||
const thumbSize = context === "chat" ? 100 : 180;
|
||||
const containerRef = useRef(null);
|
||||
const [cols, setCols] = useState(() => {
|
||||
// Pre-calc initial columns to stabilize layout before images render
|
||||
@@ -114,8 +120,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 +136,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",
|
||||
@@ -183,6 +188,30 @@ export function LocalMediaGrid({
|
||||
transition: "opacity .25s ease"
|
||||
}}
|
||||
/>
|
||||
{img.tags && img.tags.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
padding: "2px 6px",
|
||||
borderRadius: "0 0 4px 4px",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: "4px"
|
||||
}}
|
||||
>
|
||||
{img.tags.map((tag, tagIdx) => (
|
||||
<Tag key={tagIdx} variant="outlined" color="gold" style={{ opacity: 0.8 }}>
|
||||
{tag.value || tag.title}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
@@ -197,6 +226,24 @@ export function LocalMediaGrid({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{onClick && (
|
||||
<Checkbox
|
||||
checked={img.isSelected}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggle(idx);
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 5,
|
||||
left: 5,
|
||||
zIndex: 2,
|
||||
opacity: img.isSelected ? 1 : 0.4,
|
||||
transition: "opacity 0.3s"
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{/* No placeholders needed; layout uses auto-fit for non-chat or fixed columns for chat */}
|
||||
|
||||
@@ -199,7 +199,7 @@ export function ProductionListDetail({ bodyshop, jobs, setPrintCenterContext, te
|
||||
{!bodyshop.uselocalmediaserver && (
|
||||
<>
|
||||
<div style={{ height: "8px" }} />
|
||||
<JobDetailCardsDocumentsComponent loading={loading} data={data ? data.jobs_by_pk : null} />
|
||||
<JobDetailCardsDocumentsComponent loading={loading} data={data ? data.jobs_by_pk : null} bodyshop={bodyshop} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user