IO-3092 Implement delete, move and download on image proxy. Add imgproxy based components.

This commit is contained in:
Patrick Fic
2025-02-07 13:33:22 -08:00
parent fbb473941c
commit b069b6bc4c
17 changed files with 1491 additions and 164 deletions

View File

@@ -38,13 +38,10 @@ export const uploadToS3 = async (
context,
notification
) => {
const { bodyshop, jobId, billId, uploaded_by, callback, tagsArray } = context;
const { bodyshop, jobId, billId, uploaded_by, callback } = context;
//Set variables for getting the signed URL.
let timestamp = Math.floor(Date.now() / 1000);
//Get the signed url.
const signedURLResponse = await axios.post("/media/proxy/sign", {
//Get the signed url allowing us to PUT to S3.
const signedURLResponse = await axios.post("/media/imgproxy/sign", {
filenames: [key],
bodyshopid: bodyshop.id,
jobid: jobId
@@ -60,19 +57,16 @@ export const uploadToS3 = async (
return;
}
//Key should be same as we provided to maintain backwards compatibility.
const { presignedUrl: preSignedUploadUrlToS3, key: s3Key } = signedURLResponse.data.signedUrls[0];
//Build request to end to cloudinary.
var options = {
// headers: { "X-Requested-With": "XMLHttpRequest" },
onUploadProgress: (e) => {
if (onProgress) onProgress({ percent: (e.loaded / e.total) * 100 });
}
};
const cloudinaryUploadResponse = await cleanAxios.put(preSignedUploadUrlToS3, file, options);
const s3UploadResponse = await cleanAxios.put(preSignedUploadUrlToS3, file, options);
//Insert the document with the matching key.
let takenat;
if (fileType.includes("image")) {
@@ -94,18 +88,18 @@ export const uploadToS3 = async (
uploaded_by: uploaded_by,
key: s3Key,
type: fileType,
extension: cloudinaryUploadResponse.data.format || extension,
extension: s3UploadResponse.data.format || extension,
bodyshopid: bodyshop.id,
size: cloudinaryUploadResponse.data.bytes || file.size,
size: s3UploadResponse.data.bytes || file.size, //Leftover from Cloudinary. We don't do any optimization on upload, so it will always be file.size.
takenat
}
]
}
});
if (!documentInsert.errors) {
if (onSuccess)
onSuccess({
//TODO: Since this may go server side, we can just manage the state locally.
uid: documentInsert.data.insert_documents.returning[0].id,
name: documentInsert.data.insert_documents.returning[0].name,
status: "done",
@@ -130,17 +124,6 @@ export const uploadToS3 = async (
}
};
//Also needs to be updated in media JS and mobile app.
export function DetermineFileType(filetype) {
if (!filetype) return "auto";
else if (filetype.startsWith("image")) return "image";
else if (filetype.startsWith("video")) return "video";
else if (filetype.startsWith("application/pdf")) return "image";
else if (filetype.startsWith("application")) return "raw";
return "auto";
}
function replaceAccents(str) {
// Verifies if the String has accents and replace them
if (str.search(/[\xC0-\xFF]/g) > -1) {
@@ -167,6 +150,5 @@ function replaceAccents(str) {
.replace(/[\xFE]/g, "p")
.replace(/[\xFD\xFF]/g, "y");
}
return str;
}

View File

@@ -1,7 +1,6 @@
import { EditFilled, FileExcelFilled, SyncOutlined } from "@ant-design/icons";
import { Button, Card, Col, Row, Space } from "antd";
import axios from "axios";
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { Gallery } from "react-grid-gallery";
import { useTranslation } from "react-i18next";
import Lightbox from "react-image-lightbox";
@@ -9,7 +8,6 @@ import "react-image-lightbox/style.css";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import DocumentsUploadImgproxyComponent from "../documents-upload-imgproxy/documents-upload-imgproxy.component";
import DocumentsUploadComponent from "../documents-upload/documents-upload.component";
import { DetermineFileType } from "../documents-upload/documents-upload.utility";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
@@ -116,85 +114,6 @@ function JobsDocumentsComponent({
setgalleryImages(documents);
}, [data, setgalleryImages, t]);
const getProxiedUrls = async () => {
const result = await axios.post("/media/proxy/thumbnails", { jobid: jobId });
let documents = result.data.reduce(
(acc, value) => {
const fileType = DetermineFileType(value.type);
if (value.type.startsWith("image")) {
acc.images.push({
src: value.thumbnailUrl,
fullsize: value.originalUrl,
height: 225,
width: 225,
isSelected: false,
key: value.key,
extension: value.extension,
id: value.id,
type: value.type,
size: value.size,
tags: [{ value: value.type, title: value.type }]
});
} else {
let thumb;
switch (fileType) {
case "raw":
thumb = `${window.location.origin}/file.png`;
break;
default:
thumb = GenerateThumbUrl(value);
break;
}
const fileName = value.key.split("/").pop();
acc.other.push({
source: value.originalUrlViaProxyPath,
src: value.thumbnailUrl,
fullsize: value.presignedGetUrl,
tags: [
{
value: fileName,
title: fileName
},
{ value: value.type, title: value.type },
...(value.bill
? [
{
value: value.bill.vendor.name,
title: t("vendors.fields.name")
},
{ value: value.bill.date, title: t("bills.fields.date") },
{
value: value.bill.invoice_number,
title: t("bills.fields.invoice_number")
}
]
: [])
],
height: 225,
width: 225,
isSelected: false,
extension: value.extension,
key: value.key,
id: value.id,
type: value.type,
size: value.size
});
}
return acc;
},
{ images: [], other: [] }
);
console.log("*** ~ file: jobs-documents-gallery.component.jsx:198 ~ getProxiedUrls ~ documents:", documents);
setgalleryImages(documents);
};
// useEffect(() => {
// if (data) getProxiedUrls();
// }, [data]);
const hasMediaAccess = HasFeatureAccess({ bodyshop, featureName: "media" });
const hasMobileAccess = HasFeatureAccess({ bodyshop, featureName: "mobile" });
return (
@@ -218,19 +137,6 @@ function JobsDocumentsComponent({
</Card>
</Col>
)}
<Col span={24}>
<Button onClick={getProxiedUrls}>Get Proxied URLs</Button>
<Card title="IMG PROXY UPLOADER">
<DocumentsUploadImgproxyComponent
jobId={jobId}
totalSize={totalSize}
billId={billId}
callbackAfterUpload={billsCallback || refetch}
ignoreSizeLimit={ignoreSizeLimit}
/>
</Card>
</Col>
<Col span={24}>
<Card>
<DocumentsUploadComponent

View File

@@ -4,27 +4,53 @@ import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import JobDocuments from "./jobs-documents-gallery.component";
import JobDocumentsImgProxy from "../jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.component";
export default function JobsDocumentsContainer({ jobId, billId, documentsList, billsCallback }) {
export default function JobsDocumentsContainer({
jobId,
billId,
documentsList,
billsCallback,
refetchOverride,
ignoreSizeLimit
}) {
//TODO Add a checker to see whether we should use the img proxy side, or this side.
const useImgProxy = true;
const { loading, error, data, refetch } = useQuery(GET_DOCUMENTS_BY_JOB, {
variables: { jobId: jobId },
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
skip: !!billId
skip: useImgProxy || !!billId
});
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent type="error" message={error.message} />;
return (
<JobDocuments
data={(data && data.documents) || documentsList || []}
downloadIdentifier={data && data.jobs_by_pk.ro_number}
totalSize={data && data.documents_aggregate.aggregate.sum.size}
billId={billId}
jobId={jobId}
refetch={refetch}
billsCallback={billsCallback}
/>
);
if (useImgProxy) {
return (
<JobDocumentsImgProxy
data={(data && data.documents) || documentsList || []}
downloadIdentifier={data && data.jobs_by_pk.ro_number}
totalSize={data && data.documents_aggregate.aggregate.sum.size}
billId={billId}
jobId={jobId}
refetch={refetchOverride || refetch}
billsCallback={billsCallback}
ignoreSizeLimit={ignoreSizeLimit}
/>
);
} else {
return (
<JobDocuments
data={(data && data.documents) || documentsList || []}
downloadIdentifier={data && data.jobs_by_pk.ro_number}
totalSize={data && data.documents_aggregate.aggregate.sum.size}
billId={billId}
jobId={jobId}
refetch={refetchOverride || refetch}
billsCallback={billsCallback}
ignoreSizeLimit={ignoreSizeLimit}
/>
);
}
}

View File

@@ -0,0 +1,78 @@
import { Button, Space } from "antd";
import axios from "axios";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
import cleanAxios from "../../utils/CleanAxios";
import formatBytes from "../../utils/formatbytes";
//import yauzl from "yauzl";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsImgproxyDownloadButton);
export function JobsDocumentsImgproxyDownloadButton({ bodyshop, galleryImages, identifier }) {
const { t } = useTranslation();
const [download, setDownload] = useState(null);
const [loading, setLoading] = useState(false);
const imagesToDownload = [
...galleryImages.images.filter((image) => image.isSelected),
...galleryImages.other.filter((image) => image.isSelected)
];
function downloadProgress(progressEvent) {
setDownload((currentDownloadState) => {
return {
downloaded: progressEvent.loaded || 0,
speed: (progressEvent.loaded || 0) - ((currentDownloadState && currentDownloadState.downloaded) || 0)
};
});
}
function standardMediaDownload(bufferData) {
const a = document.createElement("a");
const url = window.URL.createObjectURL(new Blob([bufferData]));
a.href = url;
a.download = `${identifier || "documents"}.zip`;
a.click();
}
const handleDownload = async () => {
logImEXEvent("jobs_documents_download");
setLoading(true);
const zipUrl = await axios({
url: "/media/imgproxy/download",
method: "POST",
data: { documentids: imagesToDownload.map((_) => _.id) }
});
const theDownloadedZip = await cleanAxios({
url: zipUrl.data.url,
method: "GET",
responseType: "arraybuffer",
onDownloadProgress: downloadProgress
});
setLoading(false);
setDownload(null);
standardMediaDownload(theDownloadedZip.data);
};
return (
<>
<Button loading={download || loading} disabled={imagesToDownload.length < 1} onClick={handleDownload}>
<Space>
<span>{t("documents.actions.download")}</span>
{download && <span>{`(${formatBytes(download.downloaded)} @ ${formatBytes(download.speed)} / second)`}</span>}
</Space>
</Button>
</>
);
}

View File

@@ -0,0 +1,129 @@
import { useApolloClient } from "@apollo/client";
import { Button, Form, Popover, Space } from "antd";
import axios from "axios";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { GET_DOC_SIZE_BY_JOB } from "../../graphql/documents.queries.js";
import { selectBodyshop } from "../../redux/user/user.selectors.js";
import JobSearchSelect from "../job-search-select/job-search-select.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsImgproxyGalleryReassign);
export function JobsDocumentsImgproxyGalleryReassign({ bodyshop, galleryImages, callback }) {
const { t } = useTranslation();
const [form] = Form.useForm();
const notification = useNotification();
const selectedImages = useMemo(() => {
return [
...galleryImages.images.filter((image) => image.isSelected),
...galleryImages.other.filter((image) => image.isSelected)
];
}, [galleryImages]);
const client = useApolloClient();
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const handleFinish = async ({ jobid }) => {
setLoading(true);
//Check to see if the space remaining on the new job is sufficient. If it isn't cancel this.
const newJobData = await client.query({
query: GET_DOC_SIZE_BY_JOB,
variables: { jobId: jobid }
});
const transferedDocSizeTotal = selectedImages.reduce((acc, val) => acc + val.size, 0);
const shouldPreventTransfer =
bodyshop.jobsizelimit - newJobData.data.documents_aggregate.aggregate.sum.size < transferedDocSizeTotal;
if (shouldPreventTransfer) {
notification.open({
key: "cannotuploaddocuments",
type: "error",
message: t("documents.labels.reassign_limitexceeded_title"),
description: t("documents.labels.reassign_limitexceeded")
});
setLoading(false);
return;
}
const res = await axios.post("/media/imgproxy/rename", {
tojobid: jobid,
documents: selectedImages.map((i) => {
//Need to check if the current key folder is null, or another job.
const currentKeys = i.key.split("/");
currentKeys[1] = jobid;
currentKeys.join("/");
return {
id: i.id,
from: i.key,
to: currentKeys.join("/"),
extension: i.extension,
type: i.type
};
})
});
//Add in confirmation & errors.
if (callback) callback();
if (res.errors) {
notification["error"]({
message: t("documents.errors.updating", {
message: JSON.stringify(res.errors)
})
});
}
if (!res.mutationResult?.errors) {
notification["success"]({
message: t("documents.successes.updated")
});
}
setOpen(false);
setLoading(false);
};
const popContent = (
<div>
<Form onFinish={handleFinish} layout="vertical" form={form}>
<Form.Item
label={t("documents.labels.newjobid")}
style={{ width: "20rem" }}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={"jobid"}
>
<JobSearchSelect notExported={false} notInvoiced={false} />
</Form.Item>
</Form>
<Space>
<Button type="primary" onClick={() => form.submit()}>
{t("general.actions.submit")}
</Button>
<Button onClick={() => setOpen(false)}>{t("general.actions.cancel")}</Button>
</Space>
</div>
);
return (
<Popover content={popContent} open={open}>
<Button disabled={selectedImages.length < 1} onClick={() => setOpen(true)} loading={loading}>
{t("documents.actions.reassign")}
</Button>
</Popover>
);
}

View File

@@ -0,0 +1,247 @@
import { EditFilled, FileExcelFilled, SyncOutlined } from "@ant-design/icons";
import { Button, Card, Col, Row, Space, Typography } from "antd";
import axios from "axios";
import { 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";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import DocumentsUploadImgproxyComponent from "../documents-upload-imgproxy/documents-upload-imgproxy.component";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
import JobsDocumentsDownloadButton from "./jobs-document-imgproxy-gallery.download.component";
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";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({});
function JobsDocumentsImgproxyComponent({
bodyshop,
data,
jobId,
refetch,
billId,
billsCallback,
totalSize,
downloadIdentifier,
ignoreSizeLimit
}) {
const [galleryImages, setgalleryImages] = useState({ images: [], other: [] });
const { t } = useTranslation();
const [modalState, setModalState] = useState({ open: false, index: 0 });
const fetchThumbnails = async () => {
const result = await axios.post("/media/imgproxy/thumbnails", { jobid: jobId });
let documents = result.data.reduce(
(acc, value) => {
if (value.type.startsWith("image")) {
acc.images.push({
src: value.thumbnailUrl,
fullsize: value.originalUrl,
height: 225,
width: 225,
isSelected: false,
key: value.key,
extension: value.extension,
id: value.id,
type: value.type,
size: value.size,
tags: [{ value: value.type, title: value.type }]
});
} else {
const fileName = value.key.split("/").pop();
acc.other.push({
source: value.originalUrlViaProxyPath,
src: value.thumbnailUrl,
fullsize: value.presignedGetUrl,
tags: [
{
value: fileName,
title: fileName
},
{ value: value.type, title: value.type },
...(value.bill
? [
{
value: value.bill.vendor.name,
title: t("vendors.fields.name")
},
{ value: value.bill.date, title: t("bills.fields.date") },
{
value: value.bill.invoice_number,
title: t("bills.fields.invoice_number")
}
]
: [])
],
height: 225,
width: 225,
isSelected: false,
extension: value.extension,
key: value.key,
id: value.id,
type: value.type,
size: value.size
});
}
return acc;
},
{ images: [], other: [] }
);
setgalleryImages(documents);
};
useEffect(() => {
if (data) {
fetchThumbnails();
}
}, [data, setgalleryImages, t]);
const hasMediaAccess = HasFeatureAccess({ bodyshop, featureName: "media" });
const hasMobileAccess = HasFeatureAccess({ bodyshop, featureName: "mobile" });
return (
<div>
<Typography.Title level={3}>IMG PROXY COMPONENTS (DELETE ME)</Typography.Title>
<Row gutter={[16, 16]}>
<Col span={24}>
<Space wrap>
<Button
onClick={() => {
//Handle any doc refresh.
refetch && refetch();
//Do the imgproxy refresh too
fetchThumbnails();
}}
>
<SyncOutlined />
</Button>
<JobsDocumentsGallerySelectAllComponent galleryImages={galleryImages} setGalleryImages={setgalleryImages} />
<JobsDocumentsDownloadButton galleryImages={galleryImages} identifier={downloadIdentifier} />
<JobsDocumentsDeleteButton galleryImages={galleryImages} deletionCallback={billsCallback || refetch} />
{!billId && <JobsDocumentsGalleryReassign galleryImages={galleryImages} callback={refetch} />}
</Space>
</Col>
{!hasMediaAccess && (
<Col span={24}>
<Card>
<UpsellComponent disableMask upsell={upsellEnum().media.general} />
</Card>
</Col>
)}
<Col span={24}>
<Card>
<DocumentsUploadImgproxyComponent
jobId={jobId}
totalSize={totalSize}
billId={billId}
callbackAfterUpload={billsCallback || refetch}
ignoreSizeLimit={ignoreSizeLimit}
/>
</Card>
</Col>
{hasMediaAccess && !hasMobileAccess && (
<Col span={24}>
<Card>
<UpsellComponent upsell={upsellEnum().media.mobile} />
</Card>
</Col>
)}
<Col span={24}>
<Card title={t("jobs.labels.documents-images")}>
<Gallery
images={galleryImages.images}
onClick={(index, item) => {
setModalState({ open: true, index: index });
// window.open(
// item.fullsize,
// "_blank",
// "toolbar=0,location=0,menubar=0"
// );
}}
onSelect={(index, image) => {
setgalleryImages({
...galleryImages,
images: galleryImages.images.map((g, idx) =>
index === idx ? { ...g, isSelected: !g.isSelected } : g
)
});
}}
/>
</Card>
</Col>
<Col span={24}>
<Card title={t("jobs.labels.documents-other")}>
<Gallery
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) => {
setgalleryImages({
...galleryImages,
other: galleryImages.other.map((g, idx) => (index === idx ? { ...g, isSelected: !g.isSelected } : g))
});
}}
/>
</Card>
</Col>
{modalState.open && (
<Lightbox
toolbarButtons={[
<EditFilled
onClick={() => {
const newWindow = window.open(
`${window.location.protocol}//${window.location.host}/edit?documentId=${
galleryImages.images[modalState.index].id
}`,
"_blank",
"noopener,noreferrer"
);
if (newWindow) newWindow.opener = null;
}}
/>
]}
mainSrc={galleryImages.images[modalState.index].fullsize}
nextSrc={galleryImages.images[(modalState.index + 1) % galleryImages.images.length].fullsize}
prevSrc={
galleryImages.images[(modalState.index + galleryImages.images.length - 1) % galleryImages.images.length]
.fullsize
}
onCloseRequest={() => setModalState({ open: false, index: 0 })}
onMovePrevRequest={() =>
setModalState({
...modalState,
index: (modalState.index + galleryImages.images.length - 1) % galleryImages.images.length
})
}
onMoveNextRequest={() =>
setModalState({
...modalState,
index: (modalState.index + 1) % galleryImages.images.length
})
}
/>
)}
</Row>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsImgproxyComponent);

View File

@@ -0,0 +1,29 @@
import { useQuery } from "@apollo/client";
import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import JobDocuments from "./jobs-documents-imgproxy-gallery.component";
export default function JobsDocumentsImgproxyContainer({ jobId, billId, documentsList, billsCallback }) {
const { loading, error, data, refetch } = useQuery(GET_DOCUMENTS_BY_JOB, {
variables: { jobId: jobId },
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
skip: !!billId
});
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent type="error" message={error.message} />;
return (
<JobDocuments
data={(data && data.documents) || documentsList || []}
downloadIdentifier={data && data.jobs_by_pk.ro_number}
totalSize={data && data.documents_aggregate.aggregate.sum.size}
billId={billId}
jobId={jobId}
refetch={refetch}
billsCallback={billsCallback}
/>
);
}

View File

@@ -0,0 +1,68 @@
import { QuestionCircleOutlined } from "@ant-design/icons";
import { Button, Popconfirm } from "antd";
import axios from "axios";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils.js";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
//Context: currentUserEmail, bodyshop, jobid, invoiceid
export default function JobsDocumentsImgproxyDeleteButton({ galleryImages, deletionCallback }) {
const { t } = useTranslation();
const notification = useNotification();
const imagesToDelete = [
...galleryImages.images.filter((image) => image.isSelected),
...galleryImages.other.filter((image) => image.isSelected)
];
const [loading, setLoading] = useState(false);
const handleDelete = async () => {
logImEXEvent("job_documents_delete", { count: imagesToDelete.length });
try {
setLoading(true);
const res = await axios.post("/media/imgproxy/delete", {
ids: imagesToDelete.map((d) => d.id)
});
if (res.data.error) {
notification["error"]({
message: t("documents.errors.deleting", {
error: JSON.stringify(res.data.error.response.errors)
})
});
} else {
notification.open({
key: "docdeletedsuccesfully",
type: "success",
message: t("documents.successes.delete")
});
if (deletionCallback) deletionCallback();
}
} catch (error) {
notification["error"]({
message: t("documents.errors.deleting", {
error: error.message
})
});
}
setLoading(false);
};
return (
<Popconfirm
disabled={imagesToDelete.length < 1}
icon={<QuestionCircleOutlined style={{ color: "red" }} />}
onConfirm={handleDelete}
title={t("documents.labels.confirmdelete")}
okText={t("general.actions.delete")}
okButtonProps={{ danger: true }}
cancelText={t("general.actions.cancel")}
>
<Button disabled={imagesToDelete.length < 1} loading={loading}>
{t("documents.actions.delete")}
</Button>
</Popconfirm>
);
}

View File

@@ -0,0 +1,50 @@
import { useEffect } from "react";
import { Gallery } from "react-grid-gallery";
import { useTranslation } from "react-i18next";
import { GenerateSrcUrl, GenerateThumbUrl } from "./job-documents-imgproxy.utility";
function JobsDocumentImgproxyGalleryExternal({
data,
externalMediaState
}) {
const [galleryImages, setgalleryImages] = externalMediaState;
const { t } = useTranslation();
useEffect(() => {
let documents = data.reduce((acc, value) => {
if (value.type.startsWith("image")) {
acc.push({
fullsize: GenerateSrcUrl(value),
src: GenerateThumbUrl(value),
thumbnailHeight: 225,
thumbnailWidth: 225,
isSelected: false,
key: value.key,
extension: value.extension,
id: value.id,
type: value.type,
tags: [{ value: value.type, title: value.type }],
size: value.size
});
}
return acc;
}, []);
setgalleryImages(documents);
}, [data, setgalleryImages, t]);
return (
<div className="clearfix">
<Gallery
images={galleryImages}
backdropClosesModal={true}
onSelect={(index, image) => {
setgalleryImages(galleryImages.map((g, idx) => (index === idx ? { ...g, isSelected: !g.isSelected } : g)));
}}
/>
</div>
);
}
export default JobsDocumentImgproxyGalleryExternal;

View File

@@ -0,0 +1,55 @@
import { Button, Space } from "antd";
import { useTranslation } from "react-i18next";
export default function JobsDocumentsImgproxyGallerySelectAllComponent({ galleryImages, setGalleryImages }) {
const { t } = useTranslation();
const handleSelectAll = () => {
setGalleryImages({
...galleryImages,
other: galleryImages.other.map((i) => {
return { ...i, isSelected: true };
}),
images: galleryImages.images.map((i) => {
return { ...i, isSelected: true };
})
});
};
const handleSelectAllImages = () => {
setGalleryImages({
...galleryImages,
images: galleryImages.images.map((i) => {
return { ...i, isSelected: true };
})
});
};
const handleSelectAllDocuments = () => {
setGalleryImages({
...galleryImages,
other: galleryImages.other.map((i) => {
return { ...i, isSelected: true };
})
});
};
const handleDeselectAll = () => {
setGalleryImages({
...galleryImages,
other: galleryImages.other.map((i) => {
return { ...i, isSelected: false };
}),
images: galleryImages.images.map((i) => {
return { ...i, isSelected: false };
})
});
};
return (
<Space wrap>
<Button onClick={handleSelectAll}>{t("general.actions.selectall")}</Button>
<Button onClick={handleSelectAllImages}>{t("documents.actions.selectallimages")}</Button>
<Button onClick={handleSelectAllDocuments}>{t("documents.actions.selectallotherdocuments")}</Button>
<Button onClick={handleDeselectAll}>{t("general.actions.deselectall")}</Button>
</Space>
);
}

View File

@@ -0,0 +1,10 @@
/* you can make up upload button and sample style by using stylesheets */
.ant-upload-select-picture-card i {
font-size: 32px;
color: #999;
}
.ant-upload-select-picture-card .ant-upload-text {
margin-top: 8px;
color: #666;
}

View File

@@ -2,6 +2,7 @@ import { useQuery } from "@apollo/client";
import React from "react";
import AlertComponent from "../../components/alert/alert.component";
import JobsDocumentsComponent from "../../components/jobs-documents-gallery/jobs-documents-gallery.component";
import JobsDocumentsContainer from "../../components/jobs-documents-gallery/jobs-documents-gallery.container";
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import { QUERY_TEMPORARY_DOCS } from "../../graphql/documents.queries";
@@ -22,7 +23,7 @@ export function TemporaryDocsComponent({ bodyshop }) {
const { loading, error, data, refetch } = useQuery(QUERY_TEMPORARY_DOCS, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
skip: bodyshop.uselocalmediaserver
skip: bodyshop.uselocalmediaserver //TODO: Add skip if imgproxy is enabled.
});
if (loading) return <LoadingSpinner />;
@@ -32,12 +33,14 @@ export function TemporaryDocsComponent({ bodyshop }) {
return <JobsDocumentsLocalGallery job={{ id: "temporary" }} />;
}
return (
<JobsDocumentsComponent
data={data ? data.documents : []}
jobId={null}
billId={null}
refetch={refetch}
ignoreSizeLimit
/>
<>
<JobsDocumentsContainer
documentsList={data ? data.documents : []}
jobId={null}
billId={null}
refetchOverride={refetch}
ignoreSizeLimit
/>
</>
);
}