IO-3092 Implement delete, move and download on image proxy. Add imgproxy based components.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user