import { Trans, t } from '@lingui/macro'; import { AspectRatio, Button, Group, Image, Overlay, Paper, Text, rem, useMantineColorScheme } from '@mantine/core'; import { Dropzone, FileWithPath, IMAGE_MIME_TYPE } from '@mantine/dropzone'; import { useHover } from '@mantine/hooks'; import { modals } from '@mantine/modals'; import { useMemo, useState } from 'react'; import { api } from '../../App'; import { UserRoles } from '../../enums/Roles'; import { cancelEvent } from '../../functions/events'; import { InvenTreeIcon } from '../../functions/icons'; import { useUserState } from '../../states/UserState'; import { PartThumbTable } from '../../tables/part/PartThumbTable'; import { vars } from '../../theme'; import { ActionButton } from '../buttons/ActionButton'; import { ApiImage } from '../images/ApiImage'; import { StylishText } from '../items/StylishText'; /** * Props for detail image */ export type DetailImageProps = { appRole: UserRoles; src: string; apiPath: string; refresh?: () => void; imageActions?: DetailImageButtonProps; pk: string; }; /** * 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} uploadFile - Allows uploading a new image * @param {boolean} deleteFile - Allows deleting the current image */ export type DetailImageButtonProps = { selectExisting?: boolean; uploadFile?: boolean; deleteFile?: boolean; }; // Image is expected to be 1:1 square, so only 1 dimension is needed const IMAGE_DIMENSION = 256; // Image to display if instance has no image const backup_image = '/static/img/blank_image.png'; /** * Modal used for removing/deleting the current image relation */ const removeModal = (apiPath: string, setImage: (image: string) => void) => modals.openConfirmModal({ title: {t`Remove Image`}, children: ( Remove the associated image from this item? ), labels: { confirm: t`Remove`, cancel: t`Cancel` }, onConfirm: async () => { await api.patch(apiPath, { image: null }); setImage(backup_image); } }); /** * Modal used for uploading a new image */ function UploadModal({ apiPath, setImage }: { apiPath: string; setImage: (image: string) => void; }) { const [currentFile, setCurrentFile] = useState(null); let uploading = false; // Components to show in the Dropzone when no file is selected const noFileIdle = (
Drag and drop to upload Click to select file(s)
); /** * Generates components to display selected image in Dropzone */ const fileInfo = (file: FileWithPath) => { const imageUrl = URL.createObjectURL(file); const size = file.size / 1024 ** 2; return (
URL.revokeObjectURL(imageUrl)} radius="sm" height={75} fit="contain" style={{ flexBasis: '40%' }} />
{file.name} {size.toFixed(2)} MB
); }; /** * Create FormData object and upload selected image */ const uploadImage = async (file: FileWithPath | null) => { if (!file) { return; } uploading = true; const formData = new FormData(); formData.append('image', file, file.name); const response = await api.patch(apiPath, formData); if (response.data.image.includes(file.name)) { setImage(response.data.image); modals.closeAll(); } }; const { colorScheme } = useMantineColorScheme(); const primaryColor = vars.colors.primaryColors[colorScheme === 'dark' ? 4 : 6]; const redColor = vars.colors.red[colorScheme === 'dark' ? 4 : 6]; return ( setCurrentFile(files[0])} maxFiles={1} accept={IMAGE_MIME_TYPE} loading={uploading} > {currentFile ? fileInfo(currentFile) : noFileIdle} ); } /** * Generate components for Action buttons used with the Details Image */ function ImageActionButtons({ actions = {}, visible, apiPath, hasImage, pk, setImage }: { actions?: DetailImageButtonProps; visible: boolean; apiPath: string; hasImage: boolean; pk: string; setImage: (image: string) => void; }) { return ( <> {visible && ( {actions.selectExisting && ( } tooltip={t`Select from existing images`} variant="outline" size="lg" tooltipAlignment="top" onClick={(event: any) => { cancelEvent(event); modals.open({ title: {t`Select Image`}, size: 'xxl', children: }); }} /> )} {actions.uploadFile && ( } tooltip={t`Upload new image`} variant="outline" size="lg" tooltipAlignment="top" onClick={(event: any) => { cancelEvent(event); modals.open({ title: {t`Upload Image`}, children: ( ) }); }} /> )} {actions.deleteFile && hasImage && ( } tooltip={t`Delete image`} variant="outline" size="lg" tooltipAlignment="top" onClick={(event: any) => { cancelEvent(event); removeModal(apiPath, setImage); }} /> )} )} ); } /** * Renders an image with action buttons for display on Details panels */ export function DetailsImage(props: Readonly) { // Displays a group of ActionButtons on hover const { hovered, ref } = useHover(); const [img, setImg] = useState(props.src ?? backup_image); // Sets a new image, and triggers upstream instance refresh const setAndRefresh = (image: string) => { setImg(image); props.refresh && props.refresh(); }; const permissions = useUserState(); const hasOverlay: boolean = useMemo(() => { return ( props.imageActions?.selectExisting || props.imageActions?.uploadFile || props.imageActions?.deleteFile || false ); }, [props.imageActions]); const expandImage = (event: any) => { cancelEvent(event); modals.open({ children: , withCloseButton: false }); }; return ( <> {permissions.hasChangeRole(props.appRole) && hasOverlay && hovered && ( )} ); }