From a323bf0007afe05040d0cdf6932fbbccb35f2ce8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 6 Oct 2024 21:49:13 +1100 Subject: [PATCH] [PUI] download image (#8230) * Add placeholder for download image button * Implement image download functionality * Increase timeout * Show timeout notification * Icon cleanup --- .../src/components/details/DetailsImage.tsx | 94 ++++++++++++++----- src/frontend/src/components/forms/ApiForm.tsx | 7 +- src/frontend/src/functions/icons.tsx | 2 + src/frontend/src/functions/notifications.tsx | 11 +++ .../src/pages/company/CompanyDetail.tsx | 1 + src/frontend/src/pages/part/PartDetail.tsx | 3 +- .../src/pages/sales/ReturnOrderDetail.tsx | 1 + 7 files changed, 93 insertions(+), 26 deletions(-) diff --git a/src/frontend/src/components/details/DetailsImage.tsx b/src/frontend/src/components/details/DetailsImage.tsx index 6b5999af6f4..c4d3ccc6308 100644 --- a/src/frontend/src/components/details/DetailsImage.tsx +++ b/src/frontend/src/components/details/DetailsImage.tsx @@ -19,6 +19,8 @@ import { api } from '../../App'; import { UserRoles } from '../../enums/Roles'; import { cancelEvent } from '../../functions/events'; import { InvenTreeIcon } from '../../functions/icons'; +import { useEditApiFormModal } from '../../hooks/UseForm'; +import { useGlobalSettingsState } from '../../states/SettingsState'; import { useUserState } from '../../states/UserState'; import { PartThumbTable } from '../../tables/part/PartThumbTable'; import { vars } from '../../theme'; @@ -42,11 +44,13 @@ export type DetailImageProps = { * Actions for Detail Images. * If true, the button type will be visible * @param {boolean} selectExisting - PART ONLY. Allows selecting existing images as part image + * @param {boolean} downloadImage - Allows downloading image from a remote URL * @param {boolean} uploadFile - Allows uploading a new image * @param {boolean} deleteFile - Allows deleting the current image */ export type DetailImageButtonProps = { selectExisting?: boolean; + downloadImage?: boolean; uploadFile?: boolean; deleteFile?: boolean; }; @@ -245,7 +249,8 @@ function ImageActionButtons({ apiPath, hasImage, pk, - setImage + setImage, + downloadImage }: Readonly<{ actions?: DetailImageButtonProps; visible: boolean; @@ -253,7 +258,10 @@ function ImageActionButtons({ hasImage: boolean; pk: string; setImage: (image: string) => void; + downloadImage: () => void; }>) { + const globalSettings = useGlobalSettingsState(); + return ( <> {visible && ( @@ -284,6 +292,25 @@ function ImageActionButtons({ }} /> )} + {actions.downloadImage && + globalSettings.isSet('INVENTREE_DOWNLOAD_FROM_URL') && ( + + } + tooltip={t`Download remote image`} + variant="outline" + size="lg" + tooltipAlignment="top" + onClick={(event: any) => { + cancelEvent(event); + downloadImage(); + }} + /> + )} {actions.uploadFile && ( ) { const permissions = useUserState(); + const downloadImage = useEditApiFormModal({ + url: props.apiPath, + title: t`Download Image`, + fields: { + remote_image: {} + }, + timeout: 10000, + successMessage: t`Image downloaded successfully`, + onFormSuccess: (response: any) => { + if (response.image) { + setAndRefresh(response.image); + } + } + }); + const hasOverlay: boolean = useMemo(() => { return ( props.imageActions?.selectExisting || @@ -359,27 +401,33 @@ export function DetailsImage(props: Readonly) { }; return ( - - <> - - {permissions.hasChangeRole(props.appRole) && hasOverlay && hovered && ( - - - - )} - - + <> + {downloadImage.modal} + + <> + + {permissions.hasChangeRole(props.appRole) && + hasOverlay && + hovered && ( + + + + )} + + + ); } diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index 6f733ba89b6..d7dbffd1a9c 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -33,7 +33,10 @@ import { extractAvailableFields, mapFields } from '../../functions/forms'; -import { invalidResponse } from '../../functions/notifications'; +import { + invalidResponse, + showTimeoutNotification +} from '../../functions/notifications'; import { getDetailUrl } from '../../functions/urls'; import { TableState } from '../../hooks/UseTable'; import { PathParams } from '../../states/ApiState'; @@ -540,7 +543,7 @@ export function ApiForm({ break; } } else { - invalidResponse(0); + showTimeoutNotification(); props.onFormError?.(); } diff --git a/src/frontend/src/functions/icons.tsx b/src/frontend/src/functions/icons.tsx index caa5d75f425..192bdbaf628 100644 --- a/src/frontend/src/functions/icons.tsx +++ b/src/frontend/src/functions/icons.tsx @@ -31,6 +31,7 @@ import { IconEdit, IconExclamationCircle, IconExternalLink, + IconFileDownload, IconFileUpload, IconFlag, IconFlagShare, @@ -143,6 +144,7 @@ const icons = { notes: IconNotes, photo: IconPhoto, upload: IconFileUpload, + download: IconFileDownload, reject: IconX, refresh: IconRefresh, select_image: IconGridDots, diff --git a/src/frontend/src/functions/notifications.tsx b/src/frontend/src/functions/notifications.tsx index 7b36efbcdcb..2ed72f34d59 100644 --- a/src/frontend/src/functions/notifications.tsx +++ b/src/frontend/src/functions/notifications.tsx @@ -39,6 +39,17 @@ export function invalidResponse(returnCode: number) { }); } +/** + * Display a notification on timeout + */ +export function showTimeoutNotification() { + notifications.show({ + title: t`Timeout`, + message: t`The request timed out`, + color: 'red' + }); +} + /* * Display a login / logout notification message. * Any existing login notification(s) will be hidden. diff --git a/src/frontend/src/pages/company/CompanyDetail.tsx b/src/frontend/src/pages/company/CompanyDetail.tsx index e21b9dd757f..0dcf720d22b 100644 --- a/src/frontend/src/pages/company/CompanyDetail.tsx +++ b/src/frontend/src/pages/company/CompanyDetail.tsx @@ -153,6 +153,7 @@ export default function CompanyDetail(props: Readonly) { refresh={refreshInstance} imageActions={{ uploadFile: true, + downloadImage: true, deleteFile: true }} /> diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index 9d095c9c491..7123aac6863 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -511,6 +511,7 @@ export default function PartDetail() { appRole={UserRoles.part} imageActions={{ selectExisting: true, + downloadImage: true, uploadFile: true, deleteFile: true }} @@ -531,7 +532,7 @@ export default function PartDetail() { ) : ( ); - }, [part, instanceQuery]); + }, [globalSettings, part, instanceQuery]); // Part data panels (recalculate when part data changes) const partPanels: PanelType[] = useMemo(() => { diff --git a/src/frontend/src/pages/sales/ReturnOrderDetail.tsx b/src/frontend/src/pages/sales/ReturnOrderDetail.tsx index 618d43efbf0..bee9b47b04a 100644 --- a/src/frontend/src/pages/sales/ReturnOrderDetail.tsx +++ b/src/frontend/src/pages/sales/ReturnOrderDetail.tsx @@ -95,6 +95,7 @@ export default function ReturnOrderDetail() { type: 'text', name: 'customer_reference', label: t`Customer Reference`, + icon: 'customer', copy: true, hidden: !order.customer_reference },