diff --git a/src/frontend/src/components/items/StylishText.tsx b/src/frontend/src/components/items/StylishText.tsx
index cbd63f019c..877fa7a680 100644
--- a/src/frontend/src/components/items/StylishText.tsx
+++ b/src/frontend/src/components/items/StylishText.tsx
@@ -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 (
-
+
{children}
);
diff --git a/src/frontend/src/components/nav/Header.tsx b/src/frontend/src/components/nav/Header.tsx
index 15edb27a71..18ae20dbf2 100644
--- a/src/frontend/src/components/nav/Header.tsx
+++ b/src/frontend/src/components/nav/Header.tsx
@@ -62,7 +62,10 @@ export function Header() {
{
+ notifications.refetch();
+ closeNotificationDrawer();
+ }}
/>
diff --git a/src/frontend/src/components/nav/NotificationDrawer.tsx b/src/frontend/src/components/nav/NotificationDrawer.tsx
index f2e0391047..acd5e3b1df 100644
--- a/src/frontend/src/components/nav/NotificationDrawer.tsx
+++ b/src/frontend/src/components/nav/NotificationDrawer.tsx
@@ -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({
+ {notificationQuery.data?.results?.length == 0 && (
+
+ {t`You have no unread notifications.`}
+
+ )}
{notificationQuery.data?.results.map((notification: any) => (
diff --git a/src/frontend/src/components/nav/PageDetail.tsx b/src/frontend/src/components/nav/PageDetail.tsx
index 87e9329bf8..c82aa62540 100644
--- a/src/frontend/src/components/nav/PageDetail.tsx
+++ b/src/frontend/src/components/nav/PageDetail.tsx
@@ -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({
- {title}
+ {title}
{subtitle && {subtitle}}
diff --git a/src/frontend/src/components/tables/AttachmentTable.tsx b/src/frontend/src/components/tables/AttachmentTable.tsx
index 6a529f7b8c..895ab9722f 100644
--- a/src/frontend/src/components/tables/AttachmentTable.tsx
+++ b/src/frontend/src/components/tables/AttachmentTable.tsx
@@ -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({
{allowEdit && (
diff --git a/src/frontend/src/components/tables/InvenTreeTable.tsx b/src/frontend/src/components/tables/InvenTreeTable.tsx
index d00dda1a16..cf04a888ba 100644
--- a/src/frontend/src/components/tables/InvenTreeTable.tsx
+++ b/src/frontend/src/components/tables/InvenTreeTable.tsx
@@ -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({
+ key: `inventree-hidden-table-columns-${tableName}`,
+ defaultValue: []
+ });
+
+ // Active filters (saved to local storage)
+ const [activeFilters, setActiveFilters] = useLocalStorage({
+ key: `inventree-active-table-filters-${tableName}`,
+ defaultValue: []
+ });
// Data selection
const [selectedRecords, setSelectedRecords] = useState([]);
@@ -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 (
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(false);
@@ -212,11 +203,6 @@ export function InvenTreeTable({
// Filter list visibility
const [filtersVisible, setFiltersVisible] = useState(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({
- 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(noRecordsText);
+ const [missingRecordsText, setMissingRecordsText] = useState(
+ 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 (
<>
- {customActionGroups.map((group: any, idx: number) => group)}
- {barcodeActions.length > 0 && (
+ {tableProps.customActionGroups?.map(
+ (group: any, idx: number) => group
+ )}
+ {(tableProps.barcodeActions?.length ?? 0 > 0) && (
}
label={t`Barcode actions`}
tooltip={t`Barcode actions`}
- actions={barcodeActions}
+ actions={tableProps.barcodeActions ?? []}
/>
)}
- {printingActions.length > 0 && (
+ {(tableProps.printingActions?.length ?? 0 > 0) && (
}
label={t`Print actions`}
tooltip={t`Print actions`}
- actions={printingActions}
+ actions={tableProps.printingActions ?? []}
/>
)}
- {enableDownload && (
+ {tableProps.enableDownload && (
)}
- {enableSearch && (
+ {tableProps.enableSearch && (
setSearchTerm(term)}
/>
)}
- {enableRefresh && (
+ {tableProps.enableRefresh && (
refetch()} />
@@ -465,21 +455,22 @@ export function InvenTreeTable({
onToggleColumn={toggleColumn}
/>
)}
- {hasCustomFilters && (
-
-
-
- setFiltersVisible(!filtersVisible)}
- />
-
-
-
- )}
+ {tableProps.enableFilters &&
+ (tableProps.customFilters?.length ?? 0 > 0) && (
+
+
+
+ setFiltersVisible(!filtersVisible)}
+ />
+
+
+
+ )}
{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}
/>
>
diff --git a/src/frontend/src/components/tables/build/BuildOrderTable.tsx b/src/frontend/src/components/tables/build/BuildOrderTable.tsx
index d4d9f36343..eb01c3b6c4 100644
--- a/src/frontend/src/components/tables/build/BuildOrderTable.tsx
+++ b/src/frontend/src/components/tables/build/BuildOrderTable.tsx
@@ -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 && (
-
+ {part.full_name}
+ //
)
);
}
@@ -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 (
navigate(`/build/${row.pk}`)}
+ props={{
+ enableDownload: true,
+ params: {
+ ...params,
+ part_detail: true
+ },
+ customFilters: tableFilters,
+ onRowClick: (row) => navigate(`/build/${row.pk}`)
+ }}
/>
);
}
diff --git a/src/frontend/src/components/tables/notifications/NotificationsTable.tsx b/src/frontend/src/components/tables/notifications/NotificationsTable.tsx
index 08212a6363..70d745240d 100644
--- a/src/frontend/src/components/tables/notifications/NotificationsTable.tsx
+++ b/src/frontend/src/components/tables/notifications/NotificationsTable.tsx
@@ -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({
);
}
diff --git a/src/frontend/src/components/tables/part/PartCategoryTable.tsx b/src/frontend/src/components/tables/part/PartCategoryTable.tsx
new file mode 100644
index 0000000000..1f87635996
--- /dev/null
+++ b/src/frontend/src/components/tables/part/PartCategoryTable.tsx
@@ -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 (
+ {
+ navigate(`/part/category/${record.pk}`);
+ }
+ }}
+ />
+ );
+}
diff --git a/src/frontend/src/components/tables/part/PartTable.tsx b/src/frontend/src/components/tables/part/PartTable.tsx
index c64a1fce0f..2e7fc0df60 100644
--- a/src/frontend/src/components/tables/part/PartTable.tsx
+++ b/src/frontend/src/components/tables/part/PartTable.tsx
@@ -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 (
-
+ {record.full_name}
+ //
);
}
},
@@ -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 (
Hello,
- World
- ]}
- params={tableParams}
+ tableKey={tableKey}
columns={tableColumns}
- customFilters={tableFilters}
- rowActions={partTableRowActions}
+ props={{
+ ...props,
+ enableDownload: true,
+ customFilters: tableFilters,
+ rowActions: partTableRowActions,
+ params: {
+ ...props.params,
+ category_detail: true
+ }
+ }}
/>
);
}
diff --git a/src/frontend/src/components/tables/part/RelatedPartTable.tsx b/src/frontend/src/components/tables/part/RelatedPartTable.tsx
index c1831cff03..2f9246cde9 100644
--- a/src/frontend/src/components/tables/part/RelatedPartTable.tsx
+++ b/src/frontend/src/components/tables/part/RelatedPartTable.tsx
@@ -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 (
);
}
diff --git a/src/frontend/src/components/tables/stock/StockItemTable.tsx b/src/frontend/src/components/tables/stock/StockItemTable.tsx
index f9dbba62a3..76b726b192 100644
--- a/src/frontend/src/components/tables/stock/StockItemTable.tsx
+++ b/src/frontend/src/components/tables/stock/StockItemTable.tsx
@@ -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 (
-
+ {part.full_name}
+ //
);
}
},
@@ -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 (
);
}
diff --git a/src/frontend/src/hooks/TableRefresh.tsx b/src/frontend/src/hooks/TableRefresh.tsx
index b45e1b4544..3e5a2a0116 100644
--- a/src/frontend/src/hooks/TableRefresh.tsx
+++ b/src/frontend/src/hooks/TableRefresh.tsx
@@ -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
*
- * @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:
- *
+ *
*/
-export function useTableRefresh() {
- const [refreshId, setRefreshId] = useState(randomId());
+export function useTableRefresh(tableName: string) {
+ const [tableKey, setTableKey] = useState(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 };
}
diff --git a/src/frontend/src/pages/Index/Stock.tsx b/src/frontend/src/pages/Index/Stock.tsx
index 8676c29096..b9b6d7020d 100644
--- a/src/frontend/src/pages/Index/Stock.tsx
+++ b/src/frontend/src/pages/Index/Stock.tsx
@@ -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: ,
+ content:
+ },
+ {
+ name: 'sublocations',
+ label: t`Sublocations`,
+ icon: ,
+ content:
+ }
+ ];
+ }, []);
+
return (
<>
-
-
- Stock Items
-
-
-
-
+
+
+
+
>
);
}
diff --git a/src/frontend/src/pages/Notifications.tsx b/src/frontend/src/pages/Notifications.tsx
index e6a8b40f2c..538815a646 100644
--- a/src/frontend/src/pages/Notifications.tsx
+++ b/src/frontend/src/pages/Notifications.tsx
@@ -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: (
[
{
title: t`Mark as read`,
@@ -48,8 +48,7 @@ export default function NotificationsPage() {
content: (
[
{
title: t`Mark as unread`,
@@ -83,8 +82,8 @@ export default function NotificationsPage() {
return (
<>
-
- {t`Notifications`}
+
+
>
diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx
index 67064419ad..01e600f19a 100644
--- a/src/frontend/src/pages/build/BuildDetail.tsx
+++ b/src/frontend/src/pages/build/BuildDetail.tsx
@@ -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({});
+ useEffect(() => {
+ setBuild({});
+ }, [id]);
+
// Query hook for fetching build data
const buildQuery = useQuery(['build', id ?? -1], async () => {
let url = `/build/${id}/`;
diff --git a/src/frontend/src/pages/part/CategoryDetail.tsx b/src/frontend/src/pages/part/CategoryDetail.tsx
new file mode 100644
index 0000000000..512b4d6533
--- /dev/null
+++ b/src/frontend/src/pages/part/CategoryDetail.tsx
@@ -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({});
+
+ 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: ,
+ content: (
+
+ )
+ },
+ {
+ name: 'subcategories',
+ label: t`Subcategories`,
+ icon: ,
+ content: (
+
+ )
+ },
+ {
+ name: 'parameters',
+ label: t`Parameters`,
+ icon: ,
+ content:
+ }
+ ],
+ [category, id]
+ );
+
+ return (
+
+ {category.name ?? 'Top level'}}
+ breadcrumbs={
+ id
+ ? [
+ { name: t`Parts`, url: '/part' },
+ { name: '...', url: '' },
+ {
+ name: category.name ?? t`Top level`,
+ url: `/part/category/${category.pk}`
+ }
+ ]
+ : []
+ }
+ />
+
+
+ );
+}
diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx
index 6c6169fbb5..d49746925a 100644
--- a/src/frontend/src/pages/part/PartDetail.tsx
+++ b/src/frontend/src/pages/part/PartDetail.tsx
@@ -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({});
+ 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={[