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:
parent
cbd2794a7e
commit
05e67d310a
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user