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

PartThumbTable updates (#9094)

- Change columns based on viewport width
- Use debounced value
- Enable pagination
- Fix pagination on backend API
This commit is contained in:
Oliver 2025-02-18 13:48:44 +11:00 committed by GitHub
parent a3ffc01d88
commit 480536a023
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 72 additions and 48 deletions

View File

@ -522,6 +522,9 @@ class PartThumbs(ListAPI):
data = serializer.data data = serializer.data
if page is not None:
return self.get_paginated_response(data)
else:
return Response(data) return Response(data)
filter_backends = [InvenTreeSearchFilter] filter_backends = [InvenTreeSearchFilter]

View File

@ -305,7 +305,7 @@ function ImageActionButtons({
modals.open({ modals.open({
title: <StylishText size='xl'>{t`Select Image`}</StylishText>, title: <StylishText size='xl'>{t`Select Image`}</StylishText>,
size: 'xxl', size: '80%',
children: <PartThumbTable pk={pk} setImage={setImage} /> children: <PartThumbTable pk={pk} setImage={setImage} />
}); });
}} }}

View File

@ -4,6 +4,7 @@ import {
Button, Button,
Divider, Divider,
Group, Group,
Pagination,
Paper, Paper,
SimpleGrid, SimpleGrid,
Skeleton, Skeleton,
@ -11,12 +12,13 @@ import {
Text, Text,
TextInput TextInput
} from '@mantine/core'; } from '@mantine/core';
import { useHover } from '@mantine/hooks'; import { useDebouncedValue, useHover } from '@mantine/hooks';
import { modals } from '@mantine/modals'; import { modals } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import type React from 'react'; import type React from 'react';
import { Suspense, useEffect, useState } from 'react'; import { Suspense, useState } from 'react';
import { IconX } from '@tabler/icons-react';
import { api } from '../../App'; import { api } from '../../App';
import { Thumbnail } from '../../components/images/Thumbnail'; import { Thumbnail } from '../../components/images/Thumbnail';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
@ -27,9 +29,6 @@ import { apiUrl } from '../../states/ApiState';
*/ */
export type ThumbTableProps = { export type ThumbTableProps = {
pk: string; pk: string;
limit?: number;
offset?: number;
search?: string;
setImage: (image: string) => void; setImage: (image: string) => void;
}; };
@ -122,36 +121,41 @@ async function setNewImage(
/** /**
* Renders a "table" of thumbnails * Renders a "table" of thumbnails
*/ */
export function PartThumbTable({ export function PartThumbTable({ pk, setImage }: Readonly<ThumbTableProps>) {
limit = 24, const limit = 24;
offset = 0,
search = '',
pk,
setImage
}: Readonly<ThumbTableProps>) {
const [thumbImage, setThumbImage] = useState<string | null>(null); const [thumbImage, setThumbImage] = useState<string | null>(null);
const [filterInput, setFilterInput] = useState<string>(''); const [filterInput, setFilterInput] = useState<string>('');
const [filterQuery, setFilterQuery] = useState<string>(search);
const [page, setPage] = useState<number>(1);
const [totalPages, setTotalPages] = useState<number>(1);
// Keep search filters from updating while user is typing // Keep search filters from updating while user is typing
useEffect(() => { const [searchText] = useDebouncedValue(filterInput, 500);
const timeoutId = setTimeout(() => setFilterQuery(filterInput), 500);
return () => clearTimeout(timeoutId);
}, [filterInput]);
// Fetch thumbnails from API // Fetch thumbnails from API
const thumbQuery = useQuery({ const thumbQuery = useQuery({
queryKey: [ queryKey: [ApiEndpoints.part_thumbs_list, page, searchText],
ApiEndpoints.part_thumbs_list,
{ limit: limit, offset: offset, search: filterQuery }
],
queryFn: async () => { queryFn: async () => {
return api.get(apiUrl(ApiEndpoints.part_thumbs_list), { const offset = Math.max(0, page - 1) * limit;
return api
.get(apiUrl(ApiEndpoints.part_thumbs_list), {
params: { params: {
offset: offset, offset: offset,
limit: limit, limit: limit,
search: filterQuery search: searchText
} }
})
.then((response) => {
const records = response?.data?.count ?? 1;
setTotalPages(Math.ceil(records / limit));
return response.data?.results ?? response.data;
})
.catch((error) => {
setTotalPages(1);
setPage(1);
return [];
}); });
} }
}); });
@ -161,18 +165,20 @@ export function PartThumbTable({
<Suspense> <Suspense>
<Divider /> <Divider />
<Paper p='sm'> <Paper p='sm'>
<SimpleGrid cols={8}> <SimpleGrid
cols={{ base: 2, '450px': 3, '600px': 4, '900px': 6 }}
type='container'
spacing='xs'
>
{!thumbQuery.isFetching {!thumbQuery.isFetching
? thumbQuery.data?.data.map( ? thumbQuery?.data.map((data: ImageElement, index: number) => (
(data: ImageElement, index: number) => (
<PartThumbComponent <PartThumbComponent
element={data} element={data}
key={index} key={index}
selected={thumbImage} selected={thumbImage}
selectImage={setThumbImage} selectImage={setThumbImage}
/> />
) ))
)
: [...Array(limit)].map((elem, idx) => ( : [...Array(limit)].map((elem, idx) => (
<Skeleton <Skeleton
height={150} height={150}
@ -188,13 +194,28 @@ export function PartThumbTable({
<Divider /> <Divider />
<Paper p='sm'> <Paper p='sm'>
<Group justify='space-between'> <Group justify='space-between' gap='xs'>
<Group justify='left' gap='xs'>
<TextInput <TextInput
placeholder={t`Search...`} placeholder={t`Search...`}
value={filterInput}
onChange={(event) => { onChange={(event) => {
setFilterInput(event.currentTarget.value); setFilterInput(event.currentTarget.value);
}} }}
rightSection={
<IconX
size='1rem'
color='red'
onClick={() => setFilterInput('')}
/> />
}
/>
<Pagination
total={totalPages}
value={page}
onChange={(value) => setPage(value)}
/>
</Group>
<Button <Button
disabled={!thumbImage} disabled={!thumbImage}
onClick={() => setNewImage(thumbImage, pk, setImage)} onClick={() => setNewImage(thumbImage, pk, setImage)}