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

Details image tweaks (#6603)

* Changes for PartThumbTable:

- Limit use of custom styling
- Better display of images

* Fix background color

* Use <StylishText> for titles

* Cleanup details grid

- Use Mantine components
- Simplify structure

* Fix TableThumbProps
This commit is contained in:
Oliver 2024-02-29 11:18:08 +11:00 committed by GitHub
parent cbd2794a7e
commit 05e67d310a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 152 additions and 166 deletions

View File

@ -1,9 +1,11 @@
import { Trans, t } from '@lingui/macro'; import { Trans, t } from '@lingui/macro';
import { import {
AspectRatio,
Button, Button,
Group, Group,
Image, Image,
Modal, Modal,
Overlay,
Paper, Paper,
Text, Text,
rem, rem,
@ -20,6 +22,7 @@ import { InvenTreeIcon } from '../../functions/icons';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import { PartThumbTable } from '../../tables/part/PartThumbTable'; import { PartThumbTable } from '../../tables/part/PartThumbTable';
import { ActionButton } from '../buttons/ActionButton'; import { ActionButton } from '../buttons/ActionButton';
import { StylishText } from '../items/StylishText';
import { ApiImage } from './ApiImage'; import { ApiImage } from './ApiImage';
/** /**
@ -58,9 +61,9 @@ const backup_image = '/static/img/blank_image.png';
*/ */
const removeModal = (apiPath: string, setImage: (image: string) => void) => const removeModal = (apiPath: string, setImage: (image: string) => void) =>
modals.openConfirmModal({ modals.openConfirmModal({
title: t`Remove Image`, title: <StylishText size="xl">{t`Remove Image`}</StylishText>,
children: ( children: (
<Text size="sm"> <Text>
<Trans>Remove the associated image from this item?</Trans> <Trans>Remove the associated image from this item?</Trans>
</Text> </Text>
), ),
@ -245,13 +248,8 @@ function ImageActionButtons({
pk: string; pk: string;
setImage: (image: string) => void; setImage: (image: string) => void;
}) { }) {
const [opened, { open, close }] = useDisclosure(false);
return ( return (
<> <>
<Modal opened={opened} onClose={close} title={t`Select image`} size="70%">
<PartThumbTable pk={pk} close={close} setImage={setImage} />
</Modal>
{visible && ( {visible && (
<Group <Group
spacing="xs" spacing="xs"
@ -259,24 +257,37 @@ function ImageActionButtons({
> >
{actions.selectExisting && ( {actions.selectExisting && (
<ActionButton <ActionButton
icon={<InvenTreeIcon icon="select_image" />} icon={
<InvenTreeIcon
icon="select_image"
iconProps={{ color: 'white' }}
/>
}
tooltip={t`Select from existing images`} tooltip={t`Select from existing images`}
variant="outline" variant="outline"
size="lg" size="lg"
tooltipAlignment="top" tooltipAlignment="top"
onClick={open} onClick={() => {
modals.open({
title: <StylishText size="xl">{t`Select Image`}</StylishText>,
size: 'xxl',
children: <PartThumbTable pk={pk} setImage={setImage} />
});
}}
/> />
)} )}
{actions.uploadFile && ( {actions.uploadFile && (
<ActionButton <ActionButton
icon={<InvenTreeIcon icon="upload" />} icon={
<InvenTreeIcon icon="upload" iconProps={{ color: 'white' }} />
}
tooltip={t`Upload new image`} tooltip={t`Upload new image`}
variant="outline" variant="outline"
size="lg" size="lg"
tooltipAlignment="top" tooltipAlignment="top"
onClick={() => { onClick={() => {
modals.open({ modals.open({
title: t`Upload Image`, title: <StylishText size="xl">{t`Upload Image`}</StylishText>,
children: ( children: (
<UploadModal apiPath={apiPath} setImage={setImage} /> <UploadModal apiPath={apiPath} setImage={setImage} />
) )
@ -320,19 +331,10 @@ export function DetailsImage(props: DetailImageProps) {
return ( return (
<> <>
<Paper <AspectRatio ref={ref} maw={IMAGE_DIMENSION} ratio={1}>
ref={ref} <>
style={{
position: 'relative',
width: `${IMAGE_DIMENSION}px`,
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}
>
<ApiImage <ApiImage
src={img} src={img}
style={{ zIndex: 1 }}
height={IMAGE_DIMENSION} height={IMAGE_DIMENSION}
width={IMAGE_DIMENSION} width={IMAGE_DIMENSION}
onClick={() => { onClick={() => {
@ -342,7 +344,8 @@ export function DetailsImage(props: DetailImageProps) {
}); });
}} }}
/> />
{permissions.hasChangeRole(props.appRole) && ( {permissions.hasChangeRole(props.appRole) && hovered && (
<Overlay color="black" opacity={0.8}>
<ImageActionButtons <ImageActionButtons
visible={hovered} visible={hovered}
actions={props.imageActions} actions={props.imageActions}
@ -351,8 +354,10 @@ export function DetailsImage(props: DetailImageProps) {
pk={props.pk} pk={props.pk}
setImage={setAndRefresh} setImage={setAndRefresh}
/> />
</Overlay>
)} )}
</Paper> </>
</AspectRatio>
</> </>
); );
} }

View File

@ -5,6 +5,7 @@ import {
Badge, Badge,
CopyButton, CopyButton,
Group, Group,
Paper,
Skeleton, Skeleton,
Table, Table,
Text, Text,
@ -437,7 +438,7 @@ export function DetailsTable({
partIcons?: boolean; partIcons?: boolean;
}) { }) {
return ( return (
<Group> <Paper p="xs" withBorder radius="xs">
<Table striped> <Table striped>
<tbody> <tbody>
{partIcons && ( {partIcons && (
@ -475,6 +476,6 @@ export function DetailsTable({
})} })}
</tbody> </tbody>
</Table> </Table>
</Group> </Paper>
); );
} }

View File

@ -1,4 +1,4 @@
import { Paper } from '@mantine/core'; import { Grid, Group, Paper, SimpleGrid } from '@mantine/core';
import { import {
DetailImageButtonProps, DetailImageButtonProps,
@ -50,13 +50,11 @@ export function ItemDetails({
partModel: boolean; partModel: boolean;
}) { }) {
return ( return (
<Paper style={{ display: 'flex', gap: '20px', flexWrap: 'wrap' }}> <Paper p="xs">
<Paper <SimpleGrid cols={2} spacing="xs" verticalSpacing="xs">
withBorder <Grid>
style={{ flexBasis: '49%', display: 'flex', gap: '10px' }}
>
{fields.image && ( {fields.image && (
<div style={{ flexGrow: '0' }}> <Grid.Col span={4}>
<DetailsImage <DetailsImage
appRole={appRole} appRole={appRole}
imageActions={fields.image.imageActions} imageActions={fields.image.imageActions}
@ -65,33 +63,26 @@ export function ItemDetails({
refresh={refresh} refresh={refresh}
pk={params.pk} pk={params.pk}
/> />
</div> </Grid.Col>
)} )}
<Grid.Col span={8}>
{fields.left && ( {fields.left && (
<div style={{ flexGrow: '1' }}>
<DetailsTable <DetailsTable
item={params} item={params}
fields={fields.left} fields={fields.left}
partIcons={partModel} partIcons={partModel}
/> />
</div>
)}
</Paper>
{fields.right && (
<Paper style={{ flexBasis: '49%' }} withBorder>
<DetailsTable item={params} fields={fields.right} />
</Paper>
)} )}
</Grid.Col>
</Grid>
{fields.right && <DetailsTable item={params} fields={fields.right} />}
{fields.bottom_left && ( {fields.bottom_left && (
<Paper style={{ flexBasis: '49%' }} withBorder>
<DetailsTable item={params} fields={fields.bottom_left} /> <DetailsTable item={params} fields={fields.bottom_left} />
</Paper>
)} )}
{fields.bottom_right && ( {fields.bottom_right && (
<Paper style={{ flexBasis: '49%' }} withBorder>
<DetailsTable item={params} fields={fields.bottom_right} /> <DetailsTable item={params} fields={fields.bottom_right} />
</Paper>
)} )}
</SimpleGrid>
</Paper> </Paper>
); );
} }

View File

@ -1,6 +1,18 @@
import { t } from '@lingui/macro'; import { Trans, t } from '@lingui/macro';
import { Button, Paper, Skeleton, Text, TextInput } from '@mantine/core'; import {
AspectRatio,
Button,
Divider,
Group,
Paper,
SimpleGrid,
Skeleton,
Stack,
Text,
TextInput
} from '@mantine/core';
import { useHover } from '@mantine/hooks'; import { useHover } from '@mantine/hooks';
import { modals } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import React, { Suspense, useEffect, useState } from 'react'; import React, { Suspense, useEffect, useState } from 'react';
@ -17,7 +29,6 @@ export type ThumbTableProps = {
limit?: number; limit?: number;
offset?: number; offset?: number;
search?: string; search?: string;
close: () => void;
setImage: (image: string) => void; setImage: (image: string) => void;
}; };
@ -62,29 +73,19 @@ function PartThumbComponent({ selected, element, selectImage }: ThumbProps) {
return ( return (
<Paper <Paper
withBorder withBorder
style={{ style={{ backgroundColor: color }}
backgroundColor: color, p="sm"
padding: '5px',
display: 'flex',
flex: '0 1 150px',
flexFlow: 'column wrap',
placeContent: 'center space-between'
}}
ref={ref} ref={ref}
onClick={() => selectImage(element.image)} onClick={() => selectImage(element.image)}
> >
<div <Stack justify="space-between">
style={{ <AspectRatio ratio={1}>
display: 'flex',
alignItems: 'center',
flexGrow: 1
}}
>
<Thumbnail size={120} src={src} align="center"></Thumbnail> <Thumbnail size={120} src={src} align="center"></Thumbnail>
</div> </AspectRatio>
<Text style={{ alignSelf: 'center', overflowWrap: 'anywhere' }}> <Text size="xs">
{element.image.split('/')[1]} ({element.count}) {element.image.split('/')[1]} ({element.count})
</Text> </Text>
</Stack>
</Paper> </Paper>
); );
} }
@ -95,7 +96,6 @@ function PartThumbComponent({ selected, element, selectImage }: ThumbProps) {
async function setNewImage( async function setNewImage(
image: string | null, image: string | null,
pk: string, pk: string,
close: () => void,
setImage: (image: string) => void setImage: (image: string) => void
) { ) {
// No need to do anything if no image is selected // No need to do anything if no image is selected
@ -110,7 +110,7 @@ async function setNewImage(
// Update image component and close modal if update was successful // Update image component and close modal if update was successful
if (response.data.image.includes(image)) { if (response.data.image.includes(image)) {
setImage(response.data.image); setImage(response.data.image);
close(); modals.closeAll();
} }
} }
@ -118,11 +118,10 @@ async function setNewImage(
* Renders a "table" of thumbnails * Renders a "table" of thumbnails
*/ */
export function PartThumbTable({ export function PartThumbTable({
limit = 25, limit = 24,
offset = 0, offset = 0,
search = '', search = '',
pk, pk,
close,
setImage setImage
}: ThumbTableProps) { }: ThumbTableProps) {
const [img, selectImage] = useState<string | null>(null); const [img, selectImage] = useState<string | null>(null);
@ -155,24 +154,21 @@ export function PartThumbTable({
return ( return (
<> <>
<Suspense> <Suspense>
<Paper <Divider />
style={{ <Paper p="sm">
display: 'flex', <>
alignItems: 'stretch', <SimpleGrid cols={8}>
placeContent: 'stretch center',
flexWrap: 'wrap',
gap: '10px'
}}
>
{!thumbQuery.isFetching {!thumbQuery.isFetching
? thumbQuery.data?.data.map((data: ImageElement, index: number) => ( ? thumbQuery.data?.data.map(
(data: ImageElement, index: number) => (
<PartThumbComponent <PartThumbComponent
element={data} element={data}
key={index} key={index}
selected={img} selected={img}
selectImage={selectImage} selectImage={selectImage}
/> />
)) )
)
: [...Array(limit)].map((elem, idx) => ( : [...Array(limit)].map((elem, idx) => (
<Skeleton <Skeleton
height={150} height={150}
@ -182,22 +178,14 @@ export function PartThumbTable({
style={{ padding: '5px' }} style={{ padding: '5px' }}
/> />
))} ))}
</SimpleGrid>
</>
</Paper> </Paper>
</Suspense> </Suspense>
<Paper
style={{ <Divider />
position: 'sticky', <Paper p="sm">
bottom: 0, <Group position="apart">
left: 0,
right: 0,
height: '60px',
zIndex: 1,
display: 'flex',
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'space-between'
}}
>
<TextInput <TextInput
placeholder={t`Search...`} placeholder={t`Search...`}
onChange={(event) => { onChange={(event) => {
@ -206,10 +194,11 @@ export function PartThumbTable({
/> />
<Button <Button
disabled={!img} disabled={!img}
onClick={() => setNewImage(img, pk, close, setImage)} onClick={() => setNewImage(img, pk, setImage)}
> >
Submit <Trans>Select</Trans>
</Button> </Button>
</Group>
</Paper> </Paper>
</> </>
); );