2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-28 19:46:46 +00:00

[PUI] download image (#8230)

* Add placeholder for download image button

* Implement image download functionality

* Increase timeout

* Show timeout notification

* Icon cleanup
This commit is contained in:
Oliver 2024-10-06 21:49:13 +11:00 committed by GitHub
parent c914d1c5af
commit a323bf0007
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 93 additions and 26 deletions

View File

@ -19,6 +19,8 @@ import { api } from '../../App';
import { UserRoles } from '../../enums/Roles'; import { UserRoles } from '../../enums/Roles';
import { cancelEvent } from '../../functions/events'; import { cancelEvent } from '../../functions/events';
import { InvenTreeIcon } from '../../functions/icons'; import { InvenTreeIcon } from '../../functions/icons';
import { useEditApiFormModal } from '../../hooks/UseForm';
import { useGlobalSettingsState } from '../../states/SettingsState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import { PartThumbTable } from '../../tables/part/PartThumbTable'; import { PartThumbTable } from '../../tables/part/PartThumbTable';
import { vars } from '../../theme'; import { vars } from '../../theme';
@ -42,11 +44,13 @@ export type DetailImageProps = {
* Actions for Detail Images. * Actions for Detail Images.
* If true, the button type will be visible * If true, the button type will be visible
* @param {boolean} selectExisting - PART ONLY. Allows selecting existing images as part image * @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} uploadFile - Allows uploading a new image
* @param {boolean} deleteFile - Allows deleting the current image * @param {boolean} deleteFile - Allows deleting the current image
*/ */
export type DetailImageButtonProps = { export type DetailImageButtonProps = {
selectExisting?: boolean; selectExisting?: boolean;
downloadImage?: boolean;
uploadFile?: boolean; uploadFile?: boolean;
deleteFile?: boolean; deleteFile?: boolean;
}; };
@ -245,7 +249,8 @@ function ImageActionButtons({
apiPath, apiPath,
hasImage, hasImage,
pk, pk,
setImage setImage,
downloadImage
}: Readonly<{ }: Readonly<{
actions?: DetailImageButtonProps; actions?: DetailImageButtonProps;
visible: boolean; visible: boolean;
@ -253,7 +258,10 @@ function ImageActionButtons({
hasImage: boolean; hasImage: boolean;
pk: string; pk: string;
setImage: (image: string) => void; setImage: (image: string) => void;
downloadImage: () => void;
}>) { }>) {
const globalSettings = useGlobalSettingsState();
return ( return (
<> <>
{visible && ( {visible && (
@ -284,6 +292,25 @@ function ImageActionButtons({
}} }}
/> />
)} )}
{actions.downloadImage &&
globalSettings.isSet('INVENTREE_DOWNLOAD_FROM_URL') && (
<ActionButton
icon={
<InvenTreeIcon
icon="download"
iconProps={{ color: 'white' }}
/>
}
tooltip={t`Download remote image`}
variant="outline"
size="lg"
tooltipAlignment="top"
onClick={(event: any) => {
cancelEvent(event);
downloadImage();
}}
/>
)}
{actions.uploadFile && ( {actions.uploadFile && (
<ActionButton <ActionButton
icon={ icon={
@ -341,6 +368,21 @@ export function DetailsImage(props: Readonly<DetailImageProps>) {
const permissions = useUserState(); 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(() => { const hasOverlay: boolean = useMemo(() => {
return ( return (
props.imageActions?.selectExisting || props.imageActions?.selectExisting ||
@ -359,27 +401,33 @@ export function DetailsImage(props: Readonly<DetailImageProps>) {
}; };
return ( return (
<AspectRatio ref={ref} maw={IMAGE_DIMENSION} ratio={1} pos="relative"> <>
<> {downloadImage.modal}
<ApiImage <AspectRatio ref={ref} maw={IMAGE_DIMENSION} ratio={1} pos="relative">
src={img} <>
mah={IMAGE_DIMENSION} <ApiImage
maw={IMAGE_DIMENSION} src={img}
onClick={expandImage} mah={IMAGE_DIMENSION}
/> maw={IMAGE_DIMENSION}
{permissions.hasChangeRole(props.appRole) && hasOverlay && hovered && ( onClick={expandImage}
<Overlay color="black" opacity={0.8} onClick={expandImage}> />
<ImageActionButtons {permissions.hasChangeRole(props.appRole) &&
visible={hovered} hasOverlay &&
actions={props.imageActions} hovered && (
apiPath={props.apiPath} <Overlay color="black" opacity={0.8} onClick={expandImage}>
hasImage={props.src ? true : false} <ImageActionButtons
pk={props.pk} visible={hovered}
setImage={setAndRefresh} actions={props.imageActions}
/> apiPath={props.apiPath}
</Overlay> hasImage={props.src ? true : false}
)} pk={props.pk}
</> setImage={setAndRefresh}
</AspectRatio> downloadImage={downloadImage.open}
/>
</Overlay>
)}
</>
</AspectRatio>
</>
); );
} }

View File

@ -33,7 +33,10 @@ import {
extractAvailableFields, extractAvailableFields,
mapFields mapFields
} from '../../functions/forms'; } from '../../functions/forms';
import { invalidResponse } from '../../functions/notifications'; import {
invalidResponse,
showTimeoutNotification
} from '../../functions/notifications';
import { getDetailUrl } from '../../functions/urls'; import { getDetailUrl } from '../../functions/urls';
import { TableState } from '../../hooks/UseTable'; import { TableState } from '../../hooks/UseTable';
import { PathParams } from '../../states/ApiState'; import { PathParams } from '../../states/ApiState';
@ -540,7 +543,7 @@ export function ApiForm({
break; break;
} }
} else { } else {
invalidResponse(0); showTimeoutNotification();
props.onFormError?.(); props.onFormError?.();
} }

View File

@ -31,6 +31,7 @@ import {
IconEdit, IconEdit,
IconExclamationCircle, IconExclamationCircle,
IconExternalLink, IconExternalLink,
IconFileDownload,
IconFileUpload, IconFileUpload,
IconFlag, IconFlag,
IconFlagShare, IconFlagShare,
@ -143,6 +144,7 @@ const icons = {
notes: IconNotes, notes: IconNotes,
photo: IconPhoto, photo: IconPhoto,
upload: IconFileUpload, upload: IconFileUpload,
download: IconFileDownload,
reject: IconX, reject: IconX,
refresh: IconRefresh, refresh: IconRefresh,
select_image: IconGridDots, select_image: IconGridDots,

View File

@ -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. * Display a login / logout notification message.
* Any existing login notification(s) will be hidden. * Any existing login notification(s) will be hidden.

View File

@ -153,6 +153,7 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
refresh={refreshInstance} refresh={refreshInstance}
imageActions={{ imageActions={{
uploadFile: true, uploadFile: true,
downloadImage: true,
deleteFile: true deleteFile: true
}} }}
/> />

View File

@ -511,6 +511,7 @@ export default function PartDetail() {
appRole={UserRoles.part} appRole={UserRoles.part}
imageActions={{ imageActions={{
selectExisting: true, selectExisting: true,
downloadImage: true,
uploadFile: true, uploadFile: true,
deleteFile: true deleteFile: true
}} }}
@ -531,7 +532,7 @@ export default function PartDetail() {
) : ( ) : (
<Skeleton /> <Skeleton />
); );
}, [part, instanceQuery]); }, [globalSettings, part, instanceQuery]);
// Part data panels (recalculate when part data changes) // Part data panels (recalculate when part data changes)
const partPanels: PanelType[] = useMemo(() => { const partPanels: PanelType[] = useMemo(() => {

View File

@ -95,6 +95,7 @@ export default function ReturnOrderDetail() {
type: 'text', type: 'text',
name: 'customer_reference', name: 'customer_reference',
label: t`Customer Reference`, label: t`Customer Reference`,
icon: 'customer',
copy: true, copy: true,
hidden: !order.customer_reference hidden: !order.customer_reference
}, },