diff --git a/src/frontend/src/components/details/DetailsImage.tsx b/src/frontend/src/components/details/DetailsImage.tsx
index 6b5999af6f..c4d3ccc630 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 6f733ba89b..d7dbffd1a9 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 caa5d75f42..192bdbaf62 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 7b36efbcdc..2ed72f34d5 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 e21b9dd757..0dcf720d22 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 9d095c9c49..7123aac686 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 618d43efbf..bee9b47b04 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
},