mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 05:05:42 +00:00 
			
		
		
		
	[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:
		| @@ -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> | ||||
|   ); | ||||
|   | ||||
| @@ -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"> | ||||
|   | ||||
| @@ -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"> | ||||
|   | ||||
| @@ -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 /> | ||||
|   | ||||
| @@ -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}> | ||||
|   | ||||
| @@ -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> | ||||
|     </> | ||||
|   | ||||
| @@ -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}`) | ||||
|       }} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|       }} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -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}`); | ||||
|         } | ||||
|       }} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| @@ -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 | ||||
|         } | ||||
|       }} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|       }} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|         } | ||||
|       }} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -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 }; | ||||
| } | ||||
|   | ||||
| @@ -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> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -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> | ||||
|     </> | ||||
|   | ||||
| @@ -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}/`; | ||||
|   | ||||
							
								
								
									
										108
									
								
								src/frontend/src/pages/part/CategoryDetail.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/frontend/src/pages/part/CategoryDetail.tsx
									
									
									
									
									
										Normal 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> | ||||
|   ); | ||||
| } | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| @@ -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', | ||||
|   | ||||
		Reference in New Issue
	
	Block a user