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,39 +331,33 @@ export function DetailsImage(props: DetailImageProps) {
return ( return (
<> <>
<Paper <AspectRatio ref={ref} maw={IMAGE_DIMENSION} ratio={1}>
ref={ref} <>
style={{ <ApiImage
position: 'relative', src={img}
width: `${IMAGE_DIMENSION}px`, height={IMAGE_DIMENSION}
display: 'flex', width={IMAGE_DIMENSION}
justifyContent: 'center', onClick={() => {
alignItems: 'center' modals.open({
}} children: <ApiImage src={img} />,
> withCloseButton: false
<ApiImage });
src={img} }}
style={{ zIndex: 1 }}
height={IMAGE_DIMENSION}
width={IMAGE_DIMENSION}
onClick={() => {
modals.open({
children: <ApiImage src={img} />,
withCloseButton: false
});
}}
/>
{permissions.hasChangeRole(props.appRole) && (
<ImageActionButtons
visible={hovered}
actions={props.imageActions}
apiPath={props.apiPath}
hasImage={props.src ? true : false}
pk={props.pk}
setImage={setAndRefresh}
/> />
)} {permissions.hasChangeRole(props.appRole) && hovered && (
</Paper> <Overlay color="black" opacity={0.8}>
<ImageActionButtons
visible={hovered}
actions={props.imageActions}
apiPath={props.apiPath}
hasImage={props.src ? true : false}
pk={props.pk}
setImage={setAndRefresh}
/>
</Overlay>
)}
</>
</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,48 +50,39 @@ 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 && (
> <Grid.Col span={4}>
{fields.image && ( <DetailsImage
<div style={{ flexGrow: '0' }}> appRole={appRole}
<DetailsImage imageActions={fields.image.imageActions}
appRole={appRole} src={params.image}
imageActions={fields.image.imageActions} apiPath={apiPath}
src={params.image} refresh={refresh}
apiPath={apiPath} pk={params.pk}
refresh={refresh} />
pk={params.pk} </Grid.Col>
/> )}
</div> <Grid.Col span={8}>
)} {fields.left && (
{fields.left && ( <DetailsTable
<div style={{ flexGrow: '1' }}> item={params}
<DetailsTable fields={fields.left}
item={params} partIcons={partModel}
fields={fields.left} />
partIcons={partModel} )}
/> </Grid.Col>
</div> </Grid>
)} {fields.right && <DetailsTable item={params} fields={fields.right} />}
</Paper> {fields.bottom_left && (
{fields.right && (
<Paper style={{ flexBasis: '49%' }} withBorder>
<DetailsTable item={params} fields={fields.right} />
</Paper>
)}
{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', <Thumbnail size={120} src={src} align="center"></Thumbnail>
alignItems: 'center', </AspectRatio>
flexGrow: 1 <Text size="xs">
}} {element.image.split('/')[1]} ({element.count})
> </Text>
<Thumbnail size={120} src={src} align="center"></Thumbnail> </Stack>
</div>
<Text style={{ alignSelf: 'center', overflowWrap: 'anywhere' }}>
{element.image.split('/')[1]} ({element.count})
</Text>
</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,61 +154,51 @@ export function PartThumbTable({
return ( return (
<> <>
<Suspense> <Suspense>
<Paper <Divider />
style={{ <Paper p="sm">
display: 'flex', <>
alignItems: 'stretch', <SimpleGrid cols={8}>
placeContent: 'stretch center', {!thumbQuery.isFetching
flexWrap: 'wrap', ? thumbQuery.data?.data.map(
gap: '10px' (data: ImageElement, index: number) => (
}} <PartThumbComponent
> element={data}
{!thumbQuery.isFetching key={index}
? thumbQuery.data?.data.map((data: ImageElement, index: number) => ( selected={img}
<PartThumbComponent selectImage={selectImage}
element={data} />
key={index} )
selected={img} )
selectImage={selectImage} : [...Array(limit)].map((elem, idx) => (
/> <Skeleton
)) height={150}
: [...Array(limit)].map((elem, idx) => ( width={150}
<Skeleton radius="sm"
height={150} key={idx}
width={150} style={{ padding: '5px' }}
radius="sm" />
key={idx} ))}
style={{ padding: '5px' }} </SimpleGrid>
/> </>
))}
</Paper> </Paper>
</Suspense> </Suspense>
<Paper
style={{ <Divider />
position: 'sticky', <Paper p="sm">
bottom: 0, <Group position="apart">
left: 0, <TextInput
right: 0, placeholder={t`Search...`}
height: '60px', onChange={(event) => {
zIndex: 1, setFilterInput(event.currentTarget.value);
display: 'flex', }}
alignItems: 'center', />
flexDirection: 'row', <Button
justifyContent: 'space-between' disabled={!img}
}} onClick={() => setNewImage(img, pk, setImage)}
> >
<TextInput <Trans>Select</Trans>
placeholder={t`Search...`} </Button>
onChange={(event) => { </Group>
setFilterInput(event.currentTarget.value);
}}
/>
<Button
disabled={!img}
onClick={() => setNewImage(img, pk, close, setImage)}
>
Submit
</Button>
</Paper> </Paper>
</> </>
); );