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={[