[PUI] Part category page (#5555)

* Replace PartIndex with CategoryDetail

- Can pass a category ID to show a single category
- Otherwise, show the top-level parts category

* Refactor <InvenTreeTable> component

- Simplify property passing
- Easier tableRefresh mechanism

* Refetch table data when base parameters change

* Correctly update pages when ID changes

* Notification panel cleanup

* Remove column from InvenTreeTableProps type

* more fancy

* Fix notification alert

* Implement useLocalStorage hook

* useLocalStorage hook for table filters too
This commit is contained in:
Oliver 2023-09-17 00:21:59 +10:00 committed by GitHub
parent 41cbe30db1
commit a68c1d28c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 502 additions and 351 deletions

View File

@ -2,10 +2,16 @@ import { Text } from '@mantine/core';
import { InvenTreeStyle } from '../../globalStyle';
export function StylishText({ children }: { children: JSX.Element | string }) {
export function StylishText({
children,
size = 'md'
}: {
children: JSX.Element | string;
size?: string;
}) {
const { classes } = InvenTreeStyle();
return (
<Text className={classes.signText} variant="gradient">
<Text size={size} className={classes.signText} variant="gradient">
{children}
</Text>
);

View File

@ -62,7 +62,10 @@ export function Header() {
<NavigationDrawer opened={navDrawerOpened} close={closeNavDrawer} />
<NotificationDrawer
opened={notificationDrawerOpened}
onClose={closeNotificationDrawer}
onClose={() => {
notifications.refetch();
closeNotificationDrawer();
}}
/>
<Container className={classes.layoutHeaderSection} size={'xl'}>
<Group position="apart">

View File

@ -1,16 +1,17 @@
import { t } from '@lingui/macro';
import {
ActionIcon,
Alert,
Divider,
Drawer,
LoadingOverlay,
Space,
Tooltip
} from '@mantine/core';
import { Badge, Group, Stack, Text } from '@mantine/core';
import { IconBellCheck, IconBellPlus, IconBookmark } from '@tabler/icons-react';
import { IconMacro } from '@tabler/icons-react';
import { Group, Stack, Text } from '@mantine/core';
import { IconBellCheck, IconBellPlus } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { api } from '../../App';
@ -79,6 +80,11 @@ export function NotificationDrawer({
<Stack spacing="xs">
<Divider />
<LoadingOverlay visible={notificationQuery.isFetching} />
{notificationQuery.data?.results?.length == 0 && (
<Alert color="green">
<Text size="sm">{t`You have no unread notifications.`}</Text>
</Alert>
)}
{notificationQuery.data?.results.map((notification: any) => (
<Group position="apart">
<Stack spacing="3">

View File

@ -1,6 +1,7 @@
import { Group, Paper, Space, Stack, Text } from '@mantine/core';
import { ReactNode } from 'react';
import { StylishText } from '../items/StylishText';
import { Breadcrumb, BreadcrumbList } from './BreadcrumbList';
/**
@ -33,7 +34,7 @@ export function PageDetail({
<Stack spacing="xs">
<Group position="apart">
<Group position="left">
<Text size="xl">{title}</Text>
<StylishText size="xl">{title}</StylishText>
{subtitle && <Text size="lg">{subtitle}</Text>}
</Group>
<Space />

View File

@ -81,9 +81,7 @@ export function AttachmentTable({
pk: number;
model: string;
}): ReactNode {
const tableId = useId();
const { refreshId, refreshTable } = useTableRefresh();
const { tableKey, refreshTable } = useTableRefresh(`${model}-attachments`);
const tableColumns = useMemo(() => attachmentTableColumns(), []);
@ -224,14 +222,16 @@ export function AttachmentTable({
<Stack spacing="xs">
<InvenTreeTable
url={url}
tableKey={tableId}
refreshId={refreshId}
params={{
[model]: pk
}}
customActionGroups={customActionGroups}
tableKey={tableKey}
columns={tableColumns}
rowActions={allowEdit && allowDelete ? rowActions : undefined}
props={{
enableSelection: true,
customActionGroups: customActionGroups,
rowActions: allowEdit && allowDelete ? rowActions : undefined,
params: {
[model]: pk
}
}}
/>
{allowEdit && (
<Dropzone onDrop={uploadFiles}>

View File

@ -1,6 +1,7 @@
import { t } from '@lingui/macro';
import { ActionIcon, Indicator, Space, Stack, Tooltip } from '@mantine/core';
import { Group } from '@mantine/core';
import { useLocalStorage } from '@mantine/hooks';
import { IconFilter, IconRefresh } from '@tabler/icons-react';
import { IconBarcode, IconPrinter } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
@ -18,96 +19,33 @@ import { FilterSelectModal } from './FilterSelectModal';
import { RowAction, RowActions } from './RowActions';
import { TableSearchInput } from './Search';
/*
* Load list of hidden columns from local storage.
* Returns a list of column names which are "hidden" for the current table
*/
function loadHiddenColumns(tableKey: string) {
return JSON.parse(
localStorage.getItem(`inventree-hidden-table-columns-${tableKey}`) || '[]'
);
}
const defaultPageSize: number = 25;
/**
* Write list of hidden columns to local storage
* @param tableKey : string - unique key for the table
* @param columns : string[] - list of column names
*/
function saveHiddenColumns(tableKey: string, columns: any[]) {
localStorage.setItem(
`inventree-hidden-table-columns-${tableKey}`,
JSON.stringify(columns)
);
}
/**
* Loads the list of active filters from local storage
* @param tableKey : string - unique key for the table
* @param filterList : TableFilter[] - list of available filters
* @returns a map of active filters for the current table, {name: value}
*/
function loadActiveFilters(tableKey: string, filterList: TableFilter[]) {
let active = JSON.parse(
localStorage.getItem(`inventree-active-table-filters-${tableKey}`) || '{}'
);
// We expect that the active filter list is a map of {name: value}
// Return *only* those filters which are in the filter list
let x = filterList
.filter((f) => f.name in active)
.map((f) => ({
...f,
value: active[f.name]
}));
return x;
}
/**
* Write the list of active filters to local storage
* @param tableKey : string - unique key for the table
* @param filters : any - map of active filters, {name: value}
*/
function saveActiveFilters(tableKey: string, filters: TableFilter[]) {
let active = Object.fromEntries(filters.map((flt) => [flt.name, flt.value]));
localStorage.setItem(
`inventree-active-table-filters-${tableKey}`,
JSON.stringify(active)
);
}
/**
* Table Component which extends DataTable with custom InvenTree functionality
* Set of optional properties which can be passed to an InvenTreeTable component
*
* TODO: Refactor table props into a single type
* @param url : string - The API endpoint to query
* @param params : any - Base query parameters
* @param tableKey : string - Unique key for the table (used for local storage)
* @param refreshId : string - Unique ID for the table (used to trigger a refresh)
* @param defaultSortColumn : string - Default column to sort by
* @param noRecordsText : string - Text to display when no records are found
* @param enableDownload : boolean - Enable download actions
* @param enableFilters : boolean - Enable filter actions
* @param enableSelection : boolean - Enable row selection
* @param enableSearch : boolean - Enable search actions
* @param enablePagination : boolean - Enable pagination
* @param enableRefresh : boolean - Enable refresh actions
* @param pageSize : number - Number of records per page
* @param barcodeActions : any[] - List of barcode actions
* @param customFilters : TableFilter[] - List of custom filters
* @param customActionGroups : any[] - List of custom action groups
* @param printingActions : any[] - List of printing actions
* @param rowActions : (record: any) => RowAction[] - Callback function to generate row actions
* @param onRowClick : (record: any, index: number, event: any) => void - Callback function when a row is clicked
*/
export function InvenTreeTable({
url,
params,
columns,
enableDownload = false,
enableFilters = true,
enablePagination = true,
enableRefresh = true,
enableSearch = true,
enableSelection = false,
pageSize = 25,
tableKey = '',
defaultSortColumn = '',
noRecordsText = t`No records found`,
printingActions = [],
barcodeActions = [],
customActionGroups = [],
customFilters = [],
rowActions,
onRowClick,
refreshId
}: {
url: string;
params: any;
columns: TableColumn[];
tableKey: string;
export type InvenTreeTableProps = {
params?: any;
defaultSortColumn?: string;
noRecordsText?: string;
enableDownload?: boolean;
@ -117,23 +55,79 @@ export function InvenTreeTable({
enablePagination?: boolean;
enableRefresh?: boolean;
pageSize?: number;
printingActions?: any[];
barcodeActions?: any[];
customActionGroups?: any[];
customFilters?: TableFilter[];
customActionGroups?: any[];
printingActions?: any[];
rowActions?: (record: any) => RowAction[];
onRowClick?: (record: any, index: number, event: any) => void;
refreshId?: string;
};
/**
* Default table properties (used if not specified)
*/
const defaultInvenTreeTableProps: InvenTreeTableProps = {
params: {},
noRecordsText: t`No records found`,
enableDownload: false,
enableFilters: true,
enablePagination: true,
enableRefresh: true,
enableSearch: true,
enableSelection: false,
pageSize: defaultPageSize,
defaultSortColumn: '',
printingActions: [],
barcodeActions: [],
customFilters: [],
customActionGroups: [],
rowActions: (record: any) => [],
onRowClick: (record: any, index: number, event: any) => {}
};
/**
* Table Component which extends DataTable with custom InvenTree functionality
*/
export function InvenTreeTable({
url,
tableKey,
columns,
props
}: {
url: string;
tableKey: string;
columns: TableColumn[];
props: InvenTreeTableProps;
}) {
// Use the first part of the table key as the table name
const tableName: string = useMemo(() => {
return tableKey.split('-')[0];
}, []);
// Build table properties based on provided props (and default props)
const tableProps: InvenTreeTableProps = useMemo(() => {
return {
...defaultInvenTreeTableProps,
...props
};
}, [props]);
// Check if any columns are switchable (can be hidden)
const hasSwitchableColumns = columns.some(
(col: TableColumn) => col.switchable
);
// Manage state for switchable columns (initially load from local storage)
let [hiddenColumns, setHiddenColumns] = useState(() =>
loadHiddenColumns(tableKey)
);
// A list of hidden columns, saved to local storage
const [hiddenColumns, setHiddenColumns] = useLocalStorage<string[]>({
key: `inventree-hidden-table-columns-${tableName}`,
defaultValue: []
});
// Active filters (saved to local storage)
const [activeFilters, setActiveFilters] = useLocalStorage<any[]>({
key: `inventree-active-table-filters-${tableName}`,
defaultValue: []
});
// Data selection
const [selectedRecords, setSelectedRecords] = useState<any[]>([]);
@ -158,7 +152,7 @@ export function InvenTreeTable({
});
// If row actions are available, add a column for them
if (rowActions) {
if (tableProps.rowActions) {
cols.push({
accessor: 'actions',
title: '',
@ -168,7 +162,7 @@ export function InvenTreeTable({
render: function (record: any) {
return (
<RowActions
actions={rowActions(record)}
actions={tableProps.rowActions?.(record) ?? []}
disabled={selectedRecords.length > 0}
/>
);
@ -177,7 +171,13 @@ export function InvenTreeTable({
}
return cols;
}, [columns, hiddenColumns, rowActions, enableSelection, selectedRecords]);
}, [
columns,
hiddenColumns,
tableProps.rowActions,
tableProps.enableSelection,
selectedRecords
]);
// Callback when column visibility is toggled
function toggleColumn(columnName: string) {
@ -189,20 +189,11 @@ export function InvenTreeTable({
newColumns[colIdx].hidden = !newColumns[colIdx].hidden;
}
let hiddenColumnNames = newColumns
.filter((col) => col.hidden)
.map((col) => col.accessor);
// Save list of hidden columns to local storage
saveHiddenColumns(tableKey, hiddenColumnNames);
// Refresh state
setHiddenColumns(loadHiddenColumns(tableKey));
setHiddenColumns(
newColumns.filter((col) => col.hidden).map((col) => col.accessor)
);
}
// Check if custom filtering is enabled for this table
const hasCustomFilters = enableFilters && customFilters.length > 0;
// Filter selection open state
const [filterSelectOpen, setFilterSelectOpen] = useState<boolean>(false);
@ -212,11 +203,6 @@ export function InvenTreeTable({
// Filter list visibility
const [filtersVisible, setFiltersVisible] = useState<boolean>(false);
// Map of currently active filters, {name: value}
const [activeFilters, setActiveFilters] = useState(() =>
loadActiveFilters(tableKey, customFilters)
);
/*
* Callback for the "add filter" button.
* Launches a modal dialog to add a new filter
@ -224,7 +210,7 @@ export function InvenTreeTable({
function onFilterAdd(name: string, value: string) {
let filters = [...activeFilters];
let newFilter = customFilters.find((flt) => flt.name == name);
let newFilter = tableProps.customFilters?.find((flt) => flt.name == name);
if (newFilter) {
filters.push({
@ -232,7 +218,6 @@ export function InvenTreeTable({
value: value
});
saveActiveFilters(tableKey, filters);
setActiveFilters(filters);
}
}
@ -242,7 +227,7 @@ export function InvenTreeTable({
*/
function onFilterRemove(filterName: string) {
let filters = activeFilters.filter((flt) => flt.name != filterName);
saveActiveFilters(tableKey, filters);
setActiveFilters(filters);
}
@ -250,7 +235,6 @@ export function InvenTreeTable({
* Callback function when all custom filters are removed from the table
*/
function onFilterClearAll() {
saveActiveFilters(tableKey, []);
setActiveFilters([]);
}
@ -266,7 +250,9 @@ export function InvenTreeTable({
* Construct query filters for the current table
*/
function getTableFilters(paginate: boolean = false) {
let queryParams = { ...params };
let queryParams = {
...tableProps.params
};
// Add custom filters
activeFilters.forEach((flt) => (queryParams[flt.name] = flt.value));
@ -277,7 +263,8 @@ export function InvenTreeTable({
}
// Pagination
if (enablePagination && paginate) {
if (tableProps.enablePagination && paginate) {
let pageSize = tableProps.pageSize ?? defaultPageSize;
queryParams.limit = pageSize;
queryParams.offset = (page - 1) * pageSize;
}
@ -315,7 +302,7 @@ export function InvenTreeTable({
// Data Sorting
const [sortStatus, setSortStatus] = useState<DataTableSortStatus>({
columnAccessor: defaultSortColumn,
columnAccessor: tableProps.defaultSortColumn ?? '',
direction: 'asc'
});
@ -335,8 +322,9 @@ export function InvenTreeTable({
}
// Missing records text (based on server response)
const [missingRecordsText, setMissingRecordsText] =
useState<string>(noRecordsText);
const [missingRecordsText, setMissingRecordsText] = useState<string>(
tableProps.noRecordsText ?? t`No records found`
);
const handleSortStatusChange = (status: DataTableSortStatus) => {
setPage(1);
@ -355,7 +343,9 @@ export function InvenTreeTable({
.then(function (response) {
switch (response.status) {
case 200:
setMissingRecordsText(noRecordsText);
setMissingRecordsText(
tableProps.noRecordsText ?? t`No records found`
);
return response.data;
case 400:
setMissingRecordsText(t`Bad request`);
@ -386,7 +376,7 @@ export function InvenTreeTable({
const { data, isError, isFetching, isLoading, refetch } = useQuery(
[
`table-${tableKey}`,
`table-${tableName}`,
sortStatus.columnAccessor,
sortStatus.direction,
page,
@ -407,15 +397,13 @@ export function InvenTreeTable({
* Implement this using the custom useTableRefresh hook
*/
useEffect(() => {
if (refreshId) {
refetch();
}
}, [refreshId]);
refetch();
}, [tableKey, props.params]);
return (
<>
<FilterSelectModal
availableFilters={customFilters}
availableFilters={tableProps.customFilters ?? []}
activeFilters={activeFilters}
opened={filterSelectOpen}
onCreateFilter={onFilterAdd}
@ -424,35 +412,37 @@ export function InvenTreeTable({
<Stack>
<Group position="apart">
<Group position="left" spacing={5}>
{customActionGroups.map((group: any, idx: number) => group)}
{barcodeActions.length > 0 && (
{tableProps.customActionGroups?.map(
(group: any, idx: number) => group
)}
{(tableProps.barcodeActions?.length ?? 0 > 0) && (
<ButtonMenu
icon={<IconBarcode />}
label={t`Barcode actions`}
tooltip={t`Barcode actions`}
actions={barcodeActions}
actions={tableProps.barcodeActions ?? []}
/>
)}
{printingActions.length > 0 && (
{(tableProps.printingActions?.length ?? 0 > 0) && (
<ButtonMenu
icon={<IconPrinter />}
label={t`Print actions`}
tooltip={t`Print actions`}
actions={printingActions}
actions={tableProps.printingActions ?? []}
/>
)}
{enableDownload && (
{tableProps.enableDownload && (
<DownloadAction downloadCallback={downloadData} />
)}
</Group>
<Space />
<Group position="right" spacing={5}>
{enableSearch && (
{tableProps.enableSearch && (
<TableSearchInput
searchCallback={(term: string) => setSearchTerm(term)}
/>
)}
{enableRefresh && (
{tableProps.enableRefresh && (
<ActionIcon>
<Tooltip label={t`Refresh data`}>
<IconRefresh onClick={() => refetch()} />
@ -465,21 +455,22 @@ export function InvenTreeTable({
onToggleColumn={toggleColumn}
/>
)}
{hasCustomFilters && (
<Indicator
size="xs"
label={activeFilters.length}
disabled={activeFilters.length == 0}
>
<ActionIcon>
<Tooltip label={t`Table filters`}>
<IconFilter
onClick={() => setFiltersVisible(!filtersVisible)}
/>
</Tooltip>
</ActionIcon>
</Indicator>
)}
{tableProps.enableFilters &&
(tableProps.customFilters?.length ?? 0 > 0) && (
<Indicator
size="xs"
label={activeFilters.length}
disabled={activeFilters.length == 0}
>
<ActionIcon>
<Tooltip label={t`Table filters`}>
<IconFilter
onClick={() => setFiltersVisible(!filtersVisible)}
/>
</Tooltip>
</ActionIcon>
</Indicator>
)}
</Group>
</Group>
{filtersVisible && (
@ -498,20 +489,22 @@ export function InvenTreeTable({
idAccessor={'pk'}
minHeight={200}
totalRecords={data?.count ?? data?.length ?? 0}
recordsPerPage={pageSize}
recordsPerPage={tableProps.pageSize ?? defaultPageSize}
page={page}
onPageChange={setPage}
sortStatus={sortStatus}
onSortStatusChange={handleSortStatusChange}
selectedRecords={enableSelection ? selectedRecords : undefined}
selectedRecords={
tableProps.enableSelection ? selectedRecords : undefined
}
onSelectedRecordsChange={
enableSelection ? onSelectedRecordsChange : undefined
tableProps.enableSelection ? onSelectedRecordsChange : undefined
}
fetching={isFetching}
noRecordsText={missingRecordsText}
records={data?.results ?? data ?? []}
columns={dataColumns}
onRowClick={onRowClick}
onRowClick={tableProps.onRowClick}
/>
</Stack>
</>

View File

@ -1,8 +1,9 @@
import { t } from '@lingui/macro';
import { Progress } from '@mantine/core';
import { Progress, Text } from '@mantine/core';
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ThumbnailHoverCard } from '../../items/Thumbnail';
import { TableColumn } from '../Column';
import { TableFilter } from '../Filter';
@ -27,11 +28,12 @@ function buildOrderTableColumns(): TableColumn[] {
let part = record.part_detail;
return (
part && (
<ThumbnailHoverCard
src={part.thumbnail || part.image}
text={part.full_name}
link=""
/>
<Text>{part.full_name}</Text>
// <ThumbnailHoverCard
// src={part.thumbnail || part.image}
// text={part.full_name}
// link=""
// />
)
);
}
@ -127,35 +129,31 @@ function buildOrderTableFilters(): TableFilter[] {
return [];
}
function buildOrderTableParams(params: any): any {
return {
...params,
part_detail: true
};
}
/*
* Construct a table of build orders, according to the provided parameters
*/
export function BuildOrderTable({ params = {} }: { params?: any }) {
// Add required query parameters
const tableParams = useMemo(() => buildOrderTableParams(params), [params]);
const tableColumns = useMemo(() => buildOrderTableColumns(), []);
const tableFilters = useMemo(() => buildOrderTableFilters(), []);
const navigate = useNavigate();
tableParams.part_detail = true;
const { tableKey, refreshTable } = useTableRefresh('buildorder');
return (
<InvenTreeTable
url="build/"
enableDownload
tableKey="build-order-table"
params={tableParams}
tableKey={tableKey}
columns={tableColumns}
customFilters={tableFilters}
onRowClick={(row) => navigate(`/build/${row.pk}`)}
props={{
enableDownload: true,
params: {
...params,
part_detail: true
},
customFilters: tableFilters,
onRowClick: (row) => navigate(`/build/${row.pk}`)
}}
/>
);
}

View File

@ -7,12 +7,10 @@ import { RowAction } from '../RowActions';
export function NotificationTable({
params,
refreshId,
tableKey,
actions
}: {
params: any;
refreshId: string;
tableKey: string;
actions: (record: any) => RowAction[];
}) {
@ -43,10 +41,12 @@ export function NotificationTable({
<InvenTreeTable
url="/notifications/"
tableKey={tableKey}
refreshId={refreshId}
params={params}
rowActions={actions}
columns={columns}
props={{
rowActions: actions,
enableSelection: true,
params: params
}}
/>
);
}

View File

@ -0,0 +1,63 @@
import { t } from '@lingui/macro';
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable';
/**
* PartCategoryTable - Displays a table of part categories
*/
export function PartCategoryTable({ params = {} }: { params?: any }) {
const navigate = useNavigate();
const { tableKey, refreshTable } = useTableRefresh('partcategory');
const tableColumns: TableColumn[] = useMemo(() => {
return [
{
accessor: 'name',
title: t`Name`,
sortable: true,
switchable: false
},
{
accessor: 'description',
title: t`Description`,
sortable: false,
switchable: true
},
{
accessor: 'pathstring',
title: t`Path`,
sortable: false,
switchable: true
},
{
accessor: 'part_count',
title: t`Parts`,
sortable: true,
switchable: true
}
];
}, []);
return (
<InvenTreeTable
url="part/category/"
tableKey={tableKey}
columns={tableColumns}
props={{
enableDownload: true,
enableSelection: true,
params: {
...params
},
onRowClick: (record, index, event) => {
navigate(`/part/category/${record.pk}`);
}
}}
/>
);
}

View File

@ -1,16 +1,16 @@
import { t } from '@lingui/macro';
import { Text } from '@mantine/core';
import { IconEdit, IconTrash } from '@tabler/icons-react';
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { editPart } from '../../../functions/forms/PartForms';
import { notYetImplemented } from '../../../functions/notifications';
import { shortenString } from '../../../functions/tables';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ThumbnailHoverCard } from '../../items/Thumbnail';
import { TableColumn } from '../Column';
import { TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
import { InvenTreeTable, InvenTreeTableProps } from '../InvenTreeTable';
import { RowAction } from '../RowActions';
/**
@ -26,11 +26,12 @@ function partTableColumns(): TableColumn[] {
render: function (record: any) {
// TODO - Link to the part detail page
return (
<ThumbnailHoverCard
src={record.thumbnail || record.image}
text={record.name}
link=""
/>
<Text>{record.full_name}</Text>
// <ThumbnailHoverCard
// src={record.thumbnail || record.image}
// text={record.name}
// link=""
// />
);
}
},
@ -178,23 +179,17 @@ function partTableFilters(): TableFilter[] {
];
}
function partTableParams(params: any): any {
return {
...params,
category_detail: true
};
}
/**
* PartListTable - Displays a list of parts, based on the provided parameters
* @param {Object} params - The query parameters to pass to the API
* @returns
*/
export function PartListTable({ params = {} }: { params?: any }) {
let tableParams = useMemo(() => partTableParams(params), [params]);
export function PartListTable({ props }: { props: InvenTreeTableProps }) {
let tableColumns = useMemo(() => partTableColumns(), []);
let tableFilters = useMemo(() => partTableFilters(), []);
const { tableKey, refreshTable } = useTableRefresh('part');
// Callback function for generating set of row actions
function partTableRowActions(record: any): RowAction[] {
let actions: RowAction[] = [];
@ -227,16 +222,18 @@ export function PartListTable({ params = {} }: { params?: any }) {
return (
<InvenTreeTable
url="part/"
enableDownload
tableKey="part-table"
printingActions={[
<Text onClick={notYetImplemented}>Hello</Text>,
<Text onClick={notYetImplemented}>World</Text>
]}
params={tableParams}
tableKey={tableKey}
columns={tableColumns}
customFilters={tableFilters}
rowActions={partTableRowActions}
props={{
...props,
enableDownload: true,
customFilters: tableFilters,
rowActions: partTableRowActions,
params: {
...props.params,
category_detail: true
}
}}
/>
);
}

View File

@ -11,7 +11,7 @@ import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable';
export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
const { refreshId, refreshTable } = useTableRefresh();
const { tableKey, refreshTable } = useTableRefresh('relatedparts');
const navigate = useNavigate();
@ -116,14 +116,16 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
return (
<InvenTreeTable
url="/part/related/"
tableKey="related-part-table"
refreshId={refreshId}
params={{
part: partId
}}
rowActions={rowActions}
tableKey={tableKey}
columns={tableColumns}
customActionGroups={customActions}
props={{
params: {
part: partId,
catefory_detail: true
},
rowActions: rowActions,
customActionGroups: customActions
}}
/>
);
}

View File

@ -1,10 +1,9 @@
import { t } from '@lingui/macro';
import { Group } from '@mantine/core';
import { IconEdit, IconTrash } from '@tabler/icons-react';
import { useEffect, useMemo, useState } from 'react';
import { Text } from '@mantine/core';
import { useMemo } from 'react';
import { notYetImplemented } from '../../../functions/notifications';
import { ActionButton } from '../../items/ActionButton';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ThumbnailHoverCard } from '../../items/Thumbnail';
import { TableColumn } from '../Column';
import { TableFilter } from '../Filter';
@ -23,11 +22,12 @@ function stockItemTableColumns(): TableColumn[] {
render: function (record: any) {
let part = record.part_detail;
return (
<ThumbnailHoverCard
src={part.thumbnail || part.image}
text={part.name}
link=""
/>
<Text>{part.full_name}</Text>
// <ThumbnailHoverCard
// src={part.thumbnail || part.image}
// text={part.name}
// link=""
// />
);
}
},
@ -102,17 +102,11 @@ function stockItemTableFilters(): TableFilter[] {
* Load a table of stock items
*/
export function StockItemTable({ params = {} }: { params?: any }) {
let tableParams = useMemo(() => {
return {
part_detail: true,
location_detail: true,
...params
};
}, [params]);
let tableColumns = useMemo(() => stockItemTableColumns(), []);
let tableFilters = useMemo(() => stockItemTableFilters(), []);
const { tableKey, refreshTable } = useTableRefresh('stockitem');
function stockItemRowActions(record: any): RowAction[] {
let actions: RowAction[] = [];
@ -129,13 +123,19 @@ export function StockItemTable({ params = {} }: { params?: any }) {
return (
<InvenTreeTable
url="stock/"
tableKey="stock-table"
enableDownload
enableSelection
params={tableParams}
tableKey={tableKey}
columns={tableColumns}
customFilters={tableFilters}
rowActions={stockItemRowActions}
props={{
enableDownload: true,
enableSelection: true,
customFilters: tableFilters,
rowActions: stockItemRowActions,
params: {
...params,
part_detail: true,
location_detail: true
}
}}
/>
);
}

View File

@ -5,21 +5,25 @@ import { useCallback, useState } from 'react';
* Custom hook for refreshing an InvenTreeTable externally
* Returns a unique ID for the table, which can be updated to trigger a refresh of the <table className=""></table>
*
* @returns [refreshId, refreshTable]
* @returns { tableKey, refreshTable }
*
* To use this hook:
* const [refreshId, refreshTable] = useTableRefresh();
* const { tableKey, refreshTable } = useTableRefresh();
*
* Then, pass the refreshId to the InvenTreeTable component:
* <InvenTreeTable refreshId={refreshId} ... />
* <InvenTreeTable tableKey={tableKey} ... />
*/
export function useTableRefresh() {
const [refreshId, setRefreshId] = useState<string>(randomId());
export function useTableRefresh(tableName: string) {
const [tableKey, setTableKey] = useState<string>(generateTableName());
function generateTableName() {
return `${tableName}-${randomId()}`;
}
// Generate a new ID to refresh the table
const refreshTable = useCallback(function () {
setRefreshId(randomId());
setTableKey(generateTableName());
}, []);
return { refreshId, refreshTable };
return { tableKey, refreshTable };
}

View File

@ -1,20 +1,37 @@
import { Trans } from '@lingui/macro';
import { Group } from '@mantine/core';
import { t } from '@lingui/macro';
import { Stack } from '@mantine/core';
import { IconPackages, IconSitemap } from '@tabler/icons-react';
import { useMemo } from 'react';
import { PlaceholderPill } from '../../components/items/Placeholder';
import { StylishText } from '../../components/items/StylishText';
import { PlaceholderPanel } from '../../components/items/Placeholder';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { StockItemTable } from '../../components/tables/stock/StockItemTable';
export default function Stock() {
const categoryPanels: PanelType[] = useMemo(() => {
return [
{
name: 'stock-items',
label: t`Stock Items`,
icon: <IconPackages size="18" />,
content: <StockItemTable />
},
{
name: 'sublocations',
label: t`Sublocations`,
icon: <IconSitemap size="18" />,
content: <PlaceholderPanel />
}
];
}, []);
return (
<>
<Group>
<StylishText>
<Trans>Stock Items</Trans>
</StylishText>
<PlaceholderPill />
</Group>
<StockItemTable />
<Stack>
<PageDetail title={t`Stock Items`} />
<PanelGroup panels={categoryPanels} />
</Stack>
</>
);
}

View File

@ -5,13 +5,14 @@ import { useMemo } from 'react';
import { api } from '../App';
import { StylishText } from '../components/items/StylishText';
import { PageDetail } from '../components/nav/PageDetail';
import { PanelGroup } from '../components/nav/PanelGroup';
import { NotificationTable } from '../components/tables/notifications/NotificationsTable';
import { useTableRefresh } from '../hooks/TableRefresh';
export default function NotificationsPage() {
const unreadRefresh = useTableRefresh();
const historyRefresh = useTableRefresh();
const unreadRefresh = useTableRefresh('unreadnotifications');
const historyRefresh = useTableRefresh('readnotifications');
const notificationPanels = useMemo(() => {
return [
@ -22,8 +23,7 @@ export default function NotificationsPage() {
content: (
<NotificationTable
params={{ read: false }}
refreshId={unreadRefresh.refreshId}
tableKey="notifications-unread"
tableKey={unreadRefresh.tableKey}
actions={(record) => [
{
title: t`Mark as read`,
@ -48,8 +48,7 @@ export default function NotificationsPage() {
content: (
<NotificationTable
params={{ read: true }}
refreshId={historyRefresh.refreshId}
tableKey="notifications-history"
tableKey={historyRefresh.tableKey}
actions={(record) => [
{
title: t`Mark as unread`,
@ -83,8 +82,8 @@ export default function NotificationsPage() {
return (
<>
<Stack spacing="xs">
<StylishText>{t`Notifications`}</StylishText>
<Stack>
<PageDetail title={t`Notifications`} />
<PanelGroup panels={notificationPanels} />
</Stack>
</>

View File

@ -12,7 +12,7 @@ import {
IconSitemap
} from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { api } from '../../App';
@ -36,6 +36,10 @@ export default function BuildDetail() {
// Build data
const [build, setBuild] = useState<any>({});
useEffect(() => {
setBuild({});
}, [id]);
// Query hook for fetching build data
const buildQuery = useQuery(['build', id ?? -1], async () => {
let url = `/build/${id}/`;

View File

@ -0,0 +1,108 @@
import { t } from '@lingui/macro';
import { Stack, Text } from '@mantine/core';
import {
IconCategory,
IconListDetails,
IconSitemap
} from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { useEffect, useMemo, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { api } from '../../App';
import { PlaceholderPanel } from '../../components/items/Placeholder';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { PartCategoryTable } from '../../components/tables/part/PartCategoryTable';
import { PartListTable } from '../../components/tables/part/PartTable';
/**
* Detail view for a single PartCategory instance.
*
* Note: If no category ID is supplied, this acts as the top-level part category page
*/
export default function CategoryDetail({}: {}) {
const { id } = useParams();
const [category, setCategory] = useState<any>({});
useEffect(() => {
setCategory({});
}, [id]);
const categoryQuery = useQuery({
enabled: id != null && id != undefined,
queryKey: ['category', id],
queryFn: async () => {
return api
.get(`/part/category/${id}/`)
.then((response) => {
setCategory(response.data);
return response.data;
})
.catch((error) => {
console.error('Error fetching category data:', error);
});
}
});
const categoryPanels: PanelType[] = useMemo(
() => [
{
name: 'parts',
label: t`Parts`,
icon: <IconCategory size="18" />,
content: (
<PartListTable
props={{
params: {
category: category.pk ?? null
}
}}
/>
)
},
{
name: 'subcategories',
label: t`Subcategories`,
icon: <IconSitemap size="18" />,
content: (
<PartCategoryTable
params={{
parent: category.pk ?? null
}}
/>
)
},
{
name: 'parameters',
label: t`Parameters`,
icon: <IconListDetails size="18" />,
content: <PlaceholderPanel />
}
],
[category, id]
);
return (
<Stack spacing="xs">
<PageDetail
title={t`Part Category`}
detail={<Text>{category.name ?? 'Top level'}</Text>}
breadcrumbs={
id
? [
{ name: t`Parts`, url: '/part' },
{ name: '...', url: '' },
{
name: category.name ?? t`Top level`,
url: `/part/category/${category.pk}`
}
]
: []
}
/>
<PanelGroup panels={categoryPanels} />
</Stack>
);
}

View File

@ -25,15 +25,12 @@ import {
IconVersions
} from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { useMemo } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { api } from '../../App';
import {
PlaceholderPanel,
PlaceholderPill
} from '../../components/items/Placeholder';
import { PlaceholderPanel } from '../../components/items/Placeholder';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { AttachmentTable } from '../../components/tables/AttachmentTable';
@ -45,12 +42,19 @@ import {
} from '../../components/widgets/MarkdownEditor';
import { editPart } from '../../functions/forms/PartForms';
/**
* Detail view for a single Part instance
*/
export default function PartDetail() {
const { id } = useParams();
// Part data
const [part, setPart] = useState<any>({});
useEffect(() => {
setPart({});
}, [id]);
// Part data panels (recalculate when part data changes)
const partPanels: PanelType[] = useMemo(() => {
return [
@ -212,7 +216,7 @@ export default function PartDetail() {
breadcrumbs={[
{ name: t`Parts`, url: '/part' },
{ name: '...', url: '' },
{ name: part.full_name, url: `/part/${part.pk}` }
{ name: part.name, url: `/part/${part.pk}` }
]}
actions={[
<Button

View File

@ -1,61 +0,0 @@
import { Trans, t } from '@lingui/macro';
import { Stack } from '@mantine/core';
import {
IconCategory,
IconListDetails,
IconSitemap
} from '@tabler/icons-react';
import { useMemo } from 'react';
import { PlaceholderPill } from '../../components/items/Placeholder';
import { StylishText } from '../../components/items/StylishText';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { PartListTable } from '../../components/tables/part/PartTable';
/**
* Part index page
*/
export default function PartIndex() {
const panels: PanelType[] = useMemo(() => {
return [
{
name: 'parts',
label: t`Parts`,
icon: <IconCategory size="18" />,
content: <PartListTable />
},
{
name: 'categories',
label: t`Categories`,
icon: <IconSitemap size="18" />,
content: <PlaceholderPill />
},
{
name: 'parameters',
label: t`Parameters`,
icon: <IconListDetails size="18" />,
content: <PlaceholderPill />
}
];
}, []);
return (
<>
<Stack>
<PageDetail
title={t`Parts`}
breadcrumbs={
[
// {
// name: t`Parts`,
// url: '/part',
// }
]
}
/>
<PanelGroup panels={panels} />
</Stack>
</>
);
}

View File

@ -11,7 +11,10 @@ export const Home = Loadable(lazy(() => import('./pages/Index/Home')));
export const Playground = Loadable(
lazy(() => import('./pages/Index/Playground'))
);
export const PartIndex = Loadable(lazy(() => import('./pages/part/PartIndex')));
export const CategoryDetail = Loadable(
lazy(() => import('./pages/part/CategoryDetail'))
);
export const PartDetail = Loadable(
lazy(() => import('./pages/part/PartDetail'))
);
@ -87,7 +90,11 @@ export const router = createBrowserRouter(
},
{
path: 'part/',
element: <PartIndex />
element: <CategoryDetail />
},
{
path: 'part/category/:id',
element: <CategoryDetail />
},
{
path: 'part/:id',