mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 05:05:42 +00:00 
			
		
		
		
	[WIP] Mantine datatables (#5218)
* Create dependency-review.yml * Create scan.yml * Create sonar-project.properties * add option to use sections and refactro * translate error messages * remove unneeded vars * move function code * move data inside * add global section * add plugin section * use translated section titles * add translation strings * rename scan action * add user settings * use ordered data * fix settings url * use debounced value for strings (not choices!) * rename contex to context * move i18n provider up * move theme options into seperate context/ component * renmae statrtup vars * move translations out * reactivate sentry * move i18n provider to seperate context * move langauge state completly out of App * use theme out * move theme context * move LanguageContext * move function into state * make sentry optional for now * add key to accordion * init langauge context on top * remove unneeded css files * move errorpage to tsx * add translation for error page * Add error to title * add typecast for error * move type definition out * remove todo -> type was already added * upgrade deps * add bootstrap * remove @mantine/core * readd core * switch to bootstrap * simplify import * Add SPA views for react #2789 * split up frontend urls * Add settings for frontend url loading * add new UI scaffold * remove tracking insert * add platform app * ensure static indexes work too * add lingui * add lingui config * add mgmt tasks * add base locales * settings for frontend dev * fix typo * update deps * add pre-commit * add eslint * add testing scaffold * fix paths * remove error - tests trip correctly * merge workflow * cleanup samples * use name inline with other tests * Add real worl frontend tests * setup env * tun migrations first * optimize setup time * setup demo dataset * optimize run setup * add test for class ui * rename * fix typo * and another typo * do install * run migrations first * fix name * cleanup * use other credentials * use other credentials * fix qc * move envs to qc * remove create_site * reduce testing env * fix test * fix test call * allaccess user * add ui plattform check * add better check * remove unneeded env * enable debug * reduce wait time * also build frontend on static * add sekeleton * fix various issues * add locales * clean output before building * cleanup dir * remove bootstrap * clean up deps * fix settings panel * remove assets * move logo * split out router * split up chunks * fix zustand import syntax * bundl * update pre-render * use vendor splitting * maximes space usage * enlarge breakpoints * remove wired color changes * cleanup tabs * fix error * update auth functions * default to mail login * add placeholder marking * Add text to placeholder * readd codespell * add another test * add sort plugin * add sort plugin * sort imports * fix order * Add mega menu * run pre-commit fixes * add node min version * Docker container (#129) * Fix allocation check for completing build order (#5199) - Allocation check only applies to untracked line items * docker dev Install required node packages to docker development image * add import order settings * cleanout built ui * Add "parttable" component * Add task to serve front-end code dev * remove default arg from build * remove eslint * optimize svg * Adds generic function for rendering a table with server-side data * Implement pagination and sorting * Add more example columns * Enable selection of table data rows * add build step for plattform UI * fix install command * optional parameters * Add simple stock table * Add optional parameter for default sort * Change "no records" text based on query result * Translate * Start writing some helper functions * Add thumbnail component * Fill out more columns for stock table * Add simple skeleton for table search input * Adjust default table properties * Change loader variant * Drop-down for selecting table columns * Add search text callback * use alpine commands * do not use cache when creating image * More updates for inventree table - Fix search text entry - Add "refresh" button - Adjust variable names * Search input improvements - Add button to clear search input * Enable mantine notification system * Add "not yet implemented" notification message * Add download action button * Adds ButtonMenu component - Button which expands to show other actions - Add hooks for adding action menus to tables * Add basic build order list table * Add custom filters button for table * Allow columns to be toggled * Column visibility saved across table loads * Adds display for table filters - Define interface for table filter definition - Add component for displaying filters - Cleanup for part table * Cleanup * Define type for controlling column data * Allow custom ordering term for table column - Replaces "sortName" concept from bootstrap-table * Improve build order table - Fancy progress bars * Reimplement invoke task to serve frontend files via yarn * Update package files with mantine * Implement callback when record selection is changed * Adds generic "actionbutton" component * Remove duplicate form components * Remove tracked files in web/static * Remove a bunch of files - tracked in from the wrong original branch * More page fixes * Revert changes to reqiurements-dev.txt * Spelling fix * Component updates * Cleanup components * Cleanup * Use spread operator * Add some new dummy pages for testing * Cleanup / simplify stockitem table * Cleanup for part table * Cleanup build order table * Cleanup column toggle function * Remove hard-coded URL * Format updates * Update deps * npm required for inventree-python checks * Fix search input - Better debouncing - Cleaner code * Update package files * vite polling fixes * Implementation for download button - Dropdown menu with file format options * Implement callback for download of table data * Better state management for hidden columns * Implement state framework for active custom filters * Silence some errors * Revert change to vite config * Implement collapsible filter list group - Save active filters to local storage - Add some example filters to the part table - Add FilterBadge component * Fix page names * Simplify search input - useDebouncedValue * linting * Refactor * Remove debug msg * Simplify search state * Refactor function for constructing API query * Add tooltip * Update icons * Add modal for selecting filter options * Add more table filters for part table * render custom item for filter select * Complete implementation for selectable filters - Allow choices to be specified as attribute - Allow choices to be specified as function - Handle state management for filter choice form * Tweak badge * Cleanup top-level yarn and npm files * Less roundy --------- Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
		
							
								
								
									
										1
									
								
								.github/workflows/qc_checks.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/qc_checks.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -152,6 +152,7 @@ jobs: | ||||
|           apt-dependency: gettext poppler-utils | ||||
|           dev-install: true | ||||
|           update: true | ||||
|           npm: true | ||||
|       - name: Download Python Code For `${{ env.wrapper_name }}` | ||||
|         run: git clone --depth 1 https://github.com/inventree/${{ env.wrapper_name }} | ||||
|           ./${{ env.wrapper_name }} | ||||
|   | ||||
							
								
								
									
										2534
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2534
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,12 +1,6 @@ | ||||
| { | ||||
|   "dependencies": { | ||||
|     "@emotion/react": "^11.11.1", | ||||
|     "@mantine/core": "^6.0.17", | ||||
|     "@mantine/dropzone": "^6.0.17", | ||||
|     "@mantine/hooks": "^6.0.17", | ||||
|     "@mantine/notifications": "^6.0.17", | ||||
|     "eslint": "^8.41.0", | ||||
|     "eslint-config-google": "^0.14.0", | ||||
|     "mantine-datatable": "^2.8.5" | ||||
|     "eslint-config-google": "^0.14.0" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -30,6 +30,7 @@ | ||||
|         "axios": "^1.4.0", | ||||
|         "dayjs": "^1.11.9", | ||||
|         "html5-qrcode": "^2.3.8", | ||||
|         "mantine-datatable": "^2.9.0", | ||||
|         "react": "^18.2.0", | ||||
|         "react-dom": "^18.2.0", | ||||
|         "react-router-dom": "^6.14.2", | ||||
|   | ||||
							
								
								
									
										35
									
								
								src/frontend/src/components/items/ActionButton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/frontend/src/components/items/ActionButton.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| import { ActionIcon, Tooltip } from '@mantine/core'; | ||||
|  | ||||
| /** | ||||
|  * Construct a simple action button with consistent styling | ||||
|  */ | ||||
| export function ActionButton({ | ||||
|   icon, | ||||
|   color = 'black', | ||||
|   tooltip = '', | ||||
|   disabled = false, | ||||
|   size = 18, | ||||
|   onClick | ||||
| }: { | ||||
|   icon: any; | ||||
|   color?: string; | ||||
|   tooltip?: string; | ||||
|   variant?: string; | ||||
|   size?: number; | ||||
|   disabled?: boolean; | ||||
|   onClick?: any; | ||||
| }) { | ||||
|   return ( | ||||
|     <ActionIcon | ||||
|       disabled={disabled} | ||||
|       radius="xs" | ||||
|       color={color} | ||||
|       size={size} | ||||
|       onClick={onClick} | ||||
|     > | ||||
|       <Tooltip disabled={!tooltip} label={tooltip} position="left"> | ||||
|         {icon} | ||||
|       </Tooltip> | ||||
|     </ActionIcon> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										36
									
								
								src/frontend/src/components/items/ButtonMenu.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/frontend/src/components/items/ButtonMenu.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| import { ActionIcon, Menu, Tooltip } from '@mantine/core'; | ||||
| import { Component } from 'react'; | ||||
|  | ||||
| /** | ||||
|  * A ButtonMenu is a button that opens a menu when clicked. | ||||
|  * It features a number of actions, which can be selected by the user. | ||||
|  */ | ||||
| export function ButtonMenu({ | ||||
|   icon, | ||||
|   actions, | ||||
|   tooltip = '', | ||||
|   label = '' | ||||
| }: { | ||||
|   icon: any; | ||||
|   actions: any[]; | ||||
|   label?: string; | ||||
|   tooltip?: string; | ||||
| }) { | ||||
|   let idx = 0; | ||||
|  | ||||
|   return ( | ||||
|     <Menu shadow="xs"> | ||||
|       <Menu.Target> | ||||
|         <ActionIcon> | ||||
|           <Tooltip label={tooltip}>{icon}</Tooltip> | ||||
|         </ActionIcon> | ||||
|       </Menu.Target> | ||||
|       <Menu.Dropdown> | ||||
|         {label && <Menu.Label>{label}</Menu.Label>} | ||||
|         {actions.map((action) => ( | ||||
|           <Menu.Item key={idx++}>{action}</Menu.Item> | ||||
|         ))} | ||||
|       </Menu.Dropdown> | ||||
|     </Menu> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										57
									
								
								src/frontend/src/components/items/Thumbnail.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/frontend/src/components/items/Thumbnail.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| import { t } from '@lingui/macro'; | ||||
| import { Image } from '@mantine/core'; | ||||
| import { Group } from '@mantine/core'; | ||||
| import { Text } from '@mantine/core'; | ||||
|  | ||||
| export function Thumbnail({ | ||||
|   src, | ||||
|   alt = t`Thumbnail`, | ||||
|   size = 24 | ||||
| }: { | ||||
|   src: string; | ||||
|   alt?: string; | ||||
|   size?: number; | ||||
| }) { | ||||
|   // TODO: Use api to determine the correct URL | ||||
|   let url = 'http://localhost:8000' + src; | ||||
|  | ||||
|   // TODO: Use HoverCard to display a larger version of the image | ||||
|  | ||||
|   return ( | ||||
|     <Image | ||||
|       src={url} | ||||
|       alt={alt} | ||||
|       width={size} | ||||
|       fit="contain" | ||||
|       radius="xs" | ||||
|       withPlaceholder | ||||
|       imageProps={{ | ||||
|         style: { | ||||
|           maxHeight: size | ||||
|         } | ||||
|       }} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export function ThumbnailHoverCard({ | ||||
|   src, | ||||
|   text, | ||||
|   link = '', | ||||
|   alt = t`Thumbnail`, | ||||
|   size = 24 | ||||
| }: { | ||||
|   src: string; | ||||
|   text: string; | ||||
|   link?: string; | ||||
|   alt?: string; | ||||
|   size?: number; | ||||
| }) { | ||||
|   // TODO: Handle link | ||||
|   return ( | ||||
|     <Group position="left" spacing={10}> | ||||
|       <Thumbnail src={src} alt={alt} size={size} /> | ||||
|       <Text>{text}</Text> | ||||
|     </Group> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										15
									
								
								src/frontend/src/components/tables/Column.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/frontend/src/components/tables/Column.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| /** | ||||
|  * Interface for the table column definition | ||||
|  */ | ||||
| export type TableColumn = { | ||||
|   accessor: string; // The key in the record to access | ||||
|   ordering?: string; // The key in the record to sort by (defaults to accessor) | ||||
|   title: string; // The title of the column | ||||
|   sortable?: boolean; // Whether the column is sortable | ||||
|   switchable?: boolean; // Whether the column is switchable | ||||
|   hidden?: boolean; // Whether the column is hidden | ||||
|   render?: (record: any) => any; // A custom render function | ||||
|   filter?: any; // A custom filter function | ||||
|   filtering?: boolean; // Whether the column is filterable | ||||
|   width?: number; // The width of the column | ||||
| }; | ||||
							
								
								
									
										40
									
								
								src/frontend/src/components/tables/ColumnSelect.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/frontend/src/components/tables/ColumnSelect.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| import { t } from '@lingui/macro'; | ||||
| import { Checkbox, Menu, Tooltip } from '@mantine/core'; | ||||
| import { ActionIcon } from '@mantine/core'; | ||||
| import { IconAdjustments } from '@tabler/icons-react'; | ||||
|  | ||||
| export function TableColumnSelect({ | ||||
|   columns, | ||||
|   onToggleColumn | ||||
| }: { | ||||
|   columns: any[]; | ||||
|   onToggleColumn: (columnName: string) => void; | ||||
| }) { | ||||
|   return ( | ||||
|     <Menu shadow="xs"> | ||||
|       <Menu.Target> | ||||
|         <ActionIcon> | ||||
|           <Tooltip label={t`Select Columns`}> | ||||
|             <IconAdjustments /> | ||||
|           </Tooltip> | ||||
|         </ActionIcon> | ||||
|       </Menu.Target> | ||||
|  | ||||
|       <Menu.Dropdown> | ||||
|         <Menu.Label>{t`Select Columns`}</Menu.Label> | ||||
|         {columns | ||||
|           .filter((col) => col.switchable) | ||||
|           .map((col) => ( | ||||
|             <Menu.Item key={col.accessor}> | ||||
|               <Checkbox | ||||
|                 checked={!col.hidden} | ||||
|                 label={col.title || col.accessor} | ||||
|                 onChange={(event) => onToggleColumn(col.accessor)} | ||||
|                 radius="sm" | ||||
|               /> | ||||
|             </Menu.Item> | ||||
|           ))} | ||||
|       </Menu.Dropdown> | ||||
|     </Menu> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										45
									
								
								src/frontend/src/components/tables/DownloadAction.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/frontend/src/components/tables/DownloadAction.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| import { Trans, t } from '@lingui/macro'; | ||||
| import { ActionIcon, Divider, Group, Menu, Select } from '@mantine/core'; | ||||
| import { Tooltip } from '@mantine/core'; | ||||
| import { Button, Modal, Stack } from '@mantine/core'; | ||||
| import { useDisclosure } from '@mantine/hooks'; | ||||
| import { IconDownload } from '@tabler/icons-react'; | ||||
| import { useState } from 'react'; | ||||
|  | ||||
| export function DownloadAction({ | ||||
|   downloadCallback | ||||
| }: { | ||||
|   downloadCallback: (fileFormat: string) => void; | ||||
| }) { | ||||
|   const formatOptions = [ | ||||
|     { value: 'csv', label: t`CSV` }, | ||||
|     { value: 'tsv', label: t`TSV` }, | ||||
|     { value: 'xlsx', label: t`Excel` } | ||||
|   ]; | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <Menu> | ||||
|         <Menu.Target> | ||||
|           <ActionIcon> | ||||
|             <Tooltip label={t`Download selected data`}> | ||||
|               <IconDownload /> | ||||
|             </Tooltip> | ||||
|           </ActionIcon> | ||||
|         </Menu.Target> | ||||
|         <Menu.Dropdown> | ||||
|           {formatOptions.map((format) => ( | ||||
|             <Menu.Item | ||||
|               key={format.value} | ||||
|               onClick={() => { | ||||
|                 downloadCallback(format.value); | ||||
|               }} | ||||
|             > | ||||
|               {format.label} | ||||
|             </Menu.Item> | ||||
|           ))} | ||||
|         </Menu.Dropdown> | ||||
|       </Menu> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										21
									
								
								src/frontend/src/components/tables/Filter.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/frontend/src/components/tables/Filter.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| /** | ||||
|  * Interface for the table filter choice | ||||
|  */ | ||||
| export type TableFilterChoice = { | ||||
|   value: string; | ||||
|   label: string; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Interface for the table filter, | ||||
|  */ | ||||
| export type TableFilter = { | ||||
|   name: string; | ||||
|   label: string; | ||||
|   description?: string; | ||||
|   type: string; | ||||
|   choices?: TableFilterChoice[]; | ||||
|   choiceFunction?: () => TableFilterChoice[]; | ||||
|   defaultValue?: any; | ||||
|   value?: any; | ||||
| }; | ||||
							
								
								
									
										50
									
								
								src/frontend/src/components/tables/FilterBadge.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/frontend/src/components/tables/FilterBadge.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| import { t } from '@lingui/macro'; | ||||
| import { Badge, CloseButton } from '@mantine/core'; | ||||
| import { Text, Tooltip } from '@mantine/core'; | ||||
| import { Group } from '@mantine/core'; | ||||
|  | ||||
| import { TableFilter } from './Filter'; | ||||
|  | ||||
| export function FilterBadge({ | ||||
|   filter, | ||||
|   onFilterRemove | ||||
| }: { | ||||
|   filter: TableFilter; | ||||
|   onFilterRemove: () => void; | ||||
| }) { | ||||
|   /** | ||||
|    * Construct text to display for the given badge ID | ||||
|    */ | ||||
|   function filterDescription() { | ||||
|     let text = filter.label || filter.name; | ||||
|  | ||||
|     text += ' = '; | ||||
|     text += filter.value; | ||||
|  | ||||
|     return text; | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Badge | ||||
|       size="lg" | ||||
|       radius="lg" | ||||
|       variant="outline" | ||||
|       color="gray" | ||||
|       styles={(theme) => ({ | ||||
|         root: { | ||||
|           paddingRight: '4px' | ||||
|         }, | ||||
|         inner: { | ||||
|           textTransform: 'none' | ||||
|         } | ||||
|       })} | ||||
|     > | ||||
|       <Group spacing={1}> | ||||
|         <Text>{filterDescription()}</Text> | ||||
|         <Tooltip label={t`Remove filter`}> | ||||
|           <CloseButton color="red" onClick={() => onFilterRemove()} /> | ||||
|         </Tooltip> | ||||
|       </Group> | ||||
|     </Badge> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										58
									
								
								src/frontend/src/components/tables/FilterGroup.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/frontend/src/components/tables/FilterGroup.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| import { t } from '@lingui/macro'; | ||||
| import { ActionIcon, Group, Text, Tooltip } from '@mantine/core'; | ||||
| import { IconFilterMinus } from '@tabler/icons-react'; | ||||
| import { IconFilterPlus } from '@tabler/icons-react'; | ||||
|  | ||||
| import { TableFilter } from './Filter'; | ||||
| import { FilterBadge } from './FilterBadge'; | ||||
|  | ||||
| /** | ||||
|  * Return a table filter group component: | ||||
|  * - Displays a list of active filters for the table | ||||
|  * - Allows the user to add/remove filters | ||||
|  * - Allows the user to clear all filters | ||||
|  */ | ||||
| export function FilterGroup({ | ||||
|   activeFilters, | ||||
|   onFilterAdd, | ||||
|   onFilterRemove, | ||||
|   onFilterClearAll | ||||
| }: { | ||||
|   activeFilters: TableFilter[]; | ||||
|   onFilterAdd: () => void; | ||||
|   onFilterRemove: (filterName: string) => void; | ||||
|   onFilterClearAll: () => void; | ||||
| }) { | ||||
|   return ( | ||||
|     <Group position="right" spacing={5}> | ||||
|       {activeFilters.length == 0 && ( | ||||
|         <Text italic={true} size="sm">{t`Add table filter`}</Text> | ||||
|       )} | ||||
|       {activeFilters.map((f) => ( | ||||
|         <FilterBadge | ||||
|           key={f.name} | ||||
|           filter={f} | ||||
|           onFilterRemove={() => onFilterRemove(f.name)} | ||||
|         /> | ||||
|       ))} | ||||
|       {activeFilters.length && ( | ||||
|         <ActionIcon | ||||
|           radius="sm" | ||||
|           variant="outline" | ||||
|           onClick={() => onFilterClearAll()} | ||||
|         > | ||||
|           <Tooltip label={t`Clear all filters`}> | ||||
|             <IconFilterMinus color="red" /> | ||||
|           </Tooltip> | ||||
|         </ActionIcon> | ||||
|       )} | ||||
|       { | ||||
|         <ActionIcon radius="sm" variant="outline" onClick={() => onFilterAdd()}> | ||||
|           <Tooltip label={t`Add filter`}> | ||||
|             <IconFilterPlus color="green" /> | ||||
|           </Tooltip> | ||||
|         </ActionIcon> | ||||
|       } | ||||
|     </Group> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										178
									
								
								src/frontend/src/components/tables/FilterSelectModal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								src/frontend/src/components/tables/FilterSelectModal.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,178 @@ | ||||
| import { t } from '@lingui/macro'; | ||||
| import { Modal, Space } from '@mantine/core'; | ||||
| import { Select } from '@mantine/core'; | ||||
| import { Stack } from '@mantine/core'; | ||||
| import { Button, Group, Text } from '@mantine/core'; | ||||
| import { forwardRef, useMemo, useState } from 'react'; | ||||
|  | ||||
| import { TableFilter, TableFilterChoice } from './Filter'; | ||||
|  | ||||
| /** | ||||
|  * Construct the selection of filters | ||||
|  */ | ||||
| function constructAvailableFilters( | ||||
|   activeFilters: TableFilter[], | ||||
|   availableFilters: TableFilter[] | ||||
| ) { | ||||
|   // Collect a list of active filters | ||||
|   let activeFilterNames = activeFilters.map((flt) => flt.name); | ||||
|  | ||||
|   let options = availableFilters | ||||
|     .filter((flt) => !activeFilterNames.includes(flt.name)) | ||||
|     .map((flt) => ({ | ||||
|       value: flt.name, | ||||
|       label: flt.label, | ||||
|       description: flt.description | ||||
|     })); | ||||
|  | ||||
|   return options; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Construct the selection of available values for the selected filter | ||||
|  */ | ||||
| function constructValueOptions( | ||||
|   availableFilters: TableFilter[], | ||||
|   selectedFilter: string | null | ||||
| ) { | ||||
|   // No options if no filter is selected | ||||
|   if (!selectedFilter) { | ||||
|     return []; | ||||
|   } | ||||
|  | ||||
|   let filter = availableFilters.find((flt) => flt.name === selectedFilter); | ||||
|  | ||||
|   if (!filter) { | ||||
|     console.error(`Could not find filter ${selectedFilter}`); | ||||
|     return []; | ||||
|   } | ||||
|  | ||||
|   let options: TableFilterChoice[] = []; | ||||
|  | ||||
|   switch (filter.type) { | ||||
|     case 'boolean': | ||||
|       // Boolean filter values True / False | ||||
|       options = [ | ||||
|         { value: 'true', label: t`True` }, | ||||
|         { value: 'false', label: t`False` } | ||||
|       ]; | ||||
|       break; | ||||
|     default: | ||||
|       // Choices are supplied by the filter definition | ||||
|       if (filter.choices) { | ||||
|         options = filter.choices; | ||||
|       } else if (filter.choiceFunction) { | ||||
|         options = filter.choiceFunction(); | ||||
|       } else { | ||||
|         console.error(`Filter choices not supplied for filter ${filter.name}`); | ||||
|       } | ||||
|       break; | ||||
|   } | ||||
|  | ||||
|   return options; | ||||
| } | ||||
|  | ||||
| interface FilterProps extends React.ComponentPropsWithoutRef<'div'> { | ||||
|   name: string; | ||||
|   label: string; | ||||
|   description?: string; | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * Custom component for the filter select | ||||
|  */ | ||||
| const FilterSelectItem = forwardRef<HTMLDivElement, FilterProps>( | ||||
|   ({ name, label, description, ...others }, ref) => ( | ||||
|     <div ref={ref} {...others}> | ||||
|       <Text size="sm">{label}</Text> | ||||
|       <Text size="xs">{description}</Text> | ||||
|     </div> | ||||
|   ) | ||||
| ); | ||||
|  | ||||
| /** | ||||
|  * Modal dialog to add a} new filter for a particular table | ||||
|  * @param opened : boolean - Whether the modal is opened or not | ||||
|  * @param onClose : () => void - Function called when the modal is closed | ||||
|  * @returns | ||||
|  */ | ||||
| export function FilterSelectModal({ | ||||
|   availableFilters, | ||||
|   activeFilters, | ||||
|   opened, | ||||
|   onCreateFilter, | ||||
|   onClose | ||||
| }: { | ||||
|   availableFilters: TableFilter[]; | ||||
|   activeFilters: TableFilter[]; | ||||
|   opened: boolean; | ||||
|   onCreateFilter: (name: string, value: string) => void; | ||||
|   onClose: () => void; | ||||
| }) { | ||||
|   let filterOptions = useMemo( | ||||
|     () => constructAvailableFilters(activeFilters, availableFilters), | ||||
|     [activeFilters, availableFilters] | ||||
|   ); | ||||
|  | ||||
|   // Internal state variable for the selected filter | ||||
|   let [selectedFilter, setSelectedFilter] = useState<string | null>(null); | ||||
|  | ||||
|   // Internal state variable for the selected filter value | ||||
|   let [value, setValue] = useState<string | null>(null); | ||||
|  | ||||
|   let valueOptions = useMemo( | ||||
|     () => constructValueOptions(availableFilters, selectedFilter), | ||||
|     [availableFilters, activeFilters, selectedFilter] | ||||
|   ); | ||||
|  | ||||
|   // Callback when the modal is closed. Ensure that the internal state is reset | ||||
|   function closeModal() { | ||||
|     setSelectedFilter(null); | ||||
|     setValue(null); | ||||
|     onClose(); | ||||
|   } | ||||
|  | ||||
|   function createFilter() { | ||||
|     if (selectedFilter && value) { | ||||
|       onCreateFilter(selectedFilter, value); | ||||
|     } | ||||
|     closeModal(); | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Modal title={t`Add Table Filter`} opened={opened} onClose={closeModal}> | ||||
|       <Stack> | ||||
|         <Text>{t`Select from the available filters`}</Text> | ||||
|         <Select | ||||
|           data={filterOptions} | ||||
|           itemComponent={FilterSelectItem} | ||||
|           label={t`Filter`} | ||||
|           placeholder={t`Select filter`} | ||||
|           searchable={true} | ||||
|           onChange={(value) => setSelectedFilter(value)} | ||||
|           withinPortal={true} | ||||
|           maxDropdownHeight={400} | ||||
|         /> | ||||
|         <Select | ||||
|           data={valueOptions} | ||||
|           disabled={valueOptions.length == 0} | ||||
|           label={t`Value`} | ||||
|           placeholder={t`Select filter value`} | ||||
|           onChange={(value) => setValue(value)} | ||||
|           withinPortal={true} | ||||
|           maxDropdownHeight={400} | ||||
|         /> | ||||
|         <Group position="right"> | ||||
|           <Button color="red" onClick={closeModal}>{t`Cancel`}</Button> | ||||
|           <Button | ||||
|             color="green" | ||||
|             onClick={createFilter} | ||||
|             disabled={!(selectedFilter && value)} | ||||
|           > | ||||
|             {t`Add Filter`} | ||||
|           </Button> | ||||
|         </Group> | ||||
|       </Stack> | ||||
|     </Modal> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										468
									
								
								src/frontend/src/components/tables/InvenTreeTable.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										468
									
								
								src/frontend/src/components/tables/InvenTreeTable.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,468 @@ | ||||
| import { t } from '@lingui/macro'; | ||||
| import { ActionIcon, Indicator, Space, Stack, Tooltip } from '@mantine/core'; | ||||
| import { Group } from '@mantine/core'; | ||||
| import { IconFilter, IconRefresh } from '@tabler/icons-react'; | ||||
| import { IconBarcode, IconPrinter } from '@tabler/icons-react'; | ||||
| import { useQuery } from '@tanstack/react-query'; | ||||
| import { DataTable, DataTableSortStatus } from 'mantine-datatable'; | ||||
| import { useEffect, useState } from 'react'; | ||||
|  | ||||
| import { api } from '../../App'; | ||||
| import { ButtonMenu } from '../items/ButtonMenu'; | ||||
| import { TableColumn } from './Column'; | ||||
| import { TableColumnSelect } from './ColumnSelect'; | ||||
| import { DownloadAction } from './DownloadAction'; | ||||
| import { TableFilter } from './Filter'; | ||||
| import { FilterGroup } from './FilterGroup'; | ||||
| import { FilterSelectModal } from './FilterSelectModal'; | ||||
| 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}`) || '[]' | ||||
|   ); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * 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 | ||||
|  */ | ||||
| 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 = [] | ||||
| }: { | ||||
|   url: string; | ||||
|   params: any; | ||||
|   columns: TableColumn[]; | ||||
|   tableKey: string; | ||||
|   defaultSortColumn?: string; | ||||
|   noRecordsText?: string; | ||||
|   enableDownload?: boolean; | ||||
|   enableFilters?: boolean; | ||||
|   enableSelection?: boolean; | ||||
|   enableSearch?: boolean; | ||||
|   enablePagination?: boolean; | ||||
|   enableRefresh?: boolean; | ||||
|   pageSize?: number; | ||||
|   printingActions?: any[]; | ||||
|   barcodeActions?: any[]; | ||||
|   customActionGroups?: any[]; | ||||
|   customFilters?: TableFilter[]; | ||||
| }) { | ||||
|   // Data columns | ||||
|   const [dataColumns, setDataColumns] = useState<any[]>(columns); | ||||
|  | ||||
|   // Check if any columns are switchable (can be hidden) | ||||
|   const hasSwitchableColumns = columns.some((col: any) => col.switchable); | ||||
|  | ||||
|   // Manage state for switchable columns (initially load from local storage) | ||||
|   let [hiddenColumns, setHiddenColumns] = useState(() => | ||||
|     loadHiddenColumns(tableKey) | ||||
|   ); | ||||
|  | ||||
|   // Update column visibility when hiddenColumns change | ||||
|   useEffect(() => { | ||||
|     setDataColumns( | ||||
|       dataColumns.map((col) => { | ||||
|         return { | ||||
|           ...col, | ||||
|           hidden: hiddenColumns.includes(col.accessor) | ||||
|         }; | ||||
|       }) | ||||
|     ); | ||||
|   }, [hiddenColumns]); | ||||
|  | ||||
|   // Callback when column visibility is toggled | ||||
|   function toggleColumn(columnName: string) { | ||||
|     let newColumns = [...dataColumns]; | ||||
|  | ||||
|     let colIdx = newColumns.findIndex((col) => col.accessor == columnName); | ||||
|  | ||||
|     if (colIdx >= 0 && colIdx < newColumns.length) { | ||||
|       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)); | ||||
|   } | ||||
|  | ||||
|   // 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); | ||||
|  | ||||
|   // Pagination | ||||
|   const [page, setPage] = useState(1); | ||||
|  | ||||
|   // 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 | ||||
|    */ | ||||
|   function onFilterAdd(name: string, value: string) { | ||||
|     let filters = [...activeFilters]; | ||||
|  | ||||
|     let newFilter = customFilters.find((flt) => flt.name == name); | ||||
|  | ||||
|     if (newFilter) { | ||||
|       filters.push({ | ||||
|         ...newFilter, | ||||
|         value: value | ||||
|       }); | ||||
|  | ||||
|       saveActiveFilters(tableKey, filters); | ||||
|       setActiveFilters(filters); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|    * Callback function when a specified filter is removed from the table | ||||
|    */ | ||||
|   function onFilterRemove(filterName: string) { | ||||
|     let filters = activeFilters.filter((flt) => flt.name != filterName); | ||||
|     saveActiveFilters(tableKey, filters); | ||||
|     setActiveFilters(filters); | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|    * Callback function when all custom filters are removed from the table | ||||
|    */ | ||||
|   function onFilterClearAll() { | ||||
|     saveActiveFilters(tableKey, []); | ||||
|     setActiveFilters([]); | ||||
|   } | ||||
|  | ||||
|   // Search term | ||||
|   const [searchTerm, setSearchTerm] = useState<string>(''); | ||||
|  | ||||
|   /* | ||||
|    * Construct query filters for the current table | ||||
|    */ | ||||
|   function getTableFilters(paginate: boolean = false) { | ||||
|     let queryParams = { ...params }; | ||||
|  | ||||
|     // Add custom filters | ||||
|     activeFilters.forEach((flt) => (queryParams[flt.name] = flt.value)); | ||||
|  | ||||
|     // Add custom search term | ||||
|     if (searchTerm) { | ||||
|       queryParams.search = searchTerm; | ||||
|     } | ||||
|  | ||||
|     // Pagination | ||||
|     if (enablePagination && paginate) { | ||||
|       queryParams.limit = pageSize; | ||||
|       queryParams.offset = (page - 1) * pageSize; | ||||
|     } | ||||
|  | ||||
|     // Ordering | ||||
|     let ordering = getOrderingTerm(); | ||||
|  | ||||
|     if (ordering) { | ||||
|       if (sortStatus.direction == 'asc') { | ||||
|         queryParams.ordering = ordering; | ||||
|       } else { | ||||
|         queryParams.ordering = `-${ordering}`; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return queryParams; | ||||
|   } | ||||
|  | ||||
|   // Data download callback | ||||
|   function downloadData(fileFormat: string) { | ||||
|     // Download entire dataset (no pagination) | ||||
|     let queryParams = getTableFilters(false); | ||||
|  | ||||
|     // Specify file format | ||||
|     queryParams.export = fileFormat; | ||||
|  | ||||
|     let downloadUrl = api.getUri({ | ||||
|       url: url, | ||||
|       params: queryParams | ||||
|     }); | ||||
|  | ||||
|     // Download file in a new window (to force download) | ||||
|     window.open(downloadUrl, '_blank'); | ||||
|   } | ||||
|  | ||||
|   // Data Sorting | ||||
|   const [sortStatus, setSortStatus] = useState<DataTableSortStatus>({ | ||||
|     columnAccessor: defaultSortColumn, | ||||
|     direction: 'asc' | ||||
|   }); | ||||
|  | ||||
|   // Return the ordering parameter | ||||
|   function getOrderingTerm() { | ||||
|     let key = sortStatus.columnAccessor; | ||||
|  | ||||
|     // Sorting column not specified | ||||
|     if (key == '') { | ||||
|       return ''; | ||||
|     } | ||||
|  | ||||
|     // Find matching column: | ||||
|     // If column provides custom ordering term, use that | ||||
|     let column = dataColumns.find((col) => col.accessor == key); | ||||
|     return column?.ordering || key; | ||||
|   } | ||||
|  | ||||
|   // Missing records text (based on server response) | ||||
|   const [missingRecordsText, setMissingRecordsText] = | ||||
|     useState<string>(noRecordsText); | ||||
|  | ||||
|   // Data selection | ||||
|   const [selectedRecords, setSelectedRecords] = useState<any[]>([]); | ||||
|  | ||||
|   function onSelectedRecordsChange(records: any[]) { | ||||
|     setSelectedRecords(records); | ||||
|   } | ||||
|  | ||||
|   const handleSortStatusChange = (status: DataTableSortStatus) => { | ||||
|     setPage(1); | ||||
|     setSortStatus(status); | ||||
|   }; | ||||
|  | ||||
|   // Function to perform API query to fetch required data | ||||
|   const fetchTableData = async () => { | ||||
|     let queryParams = getTableFilters(true); | ||||
|  | ||||
|     return api | ||||
|       .get(`${url}`, { | ||||
|         params: queryParams, | ||||
|         timeout: 30 * 1000 | ||||
|       }) | ||||
|       .then(function (response) { | ||||
|         switch (response.status) { | ||||
|           case 200: | ||||
|             setMissingRecordsText(noRecordsText); | ||||
|             return response.data; | ||||
|           case 400: | ||||
|             setMissingRecordsText(t`Bad request`); | ||||
|             break; | ||||
|           case 401: | ||||
|             setMissingRecordsText(t`Unauthorized`); | ||||
|             break; | ||||
|           case 403: | ||||
|             setMissingRecordsText(t`Forbidden`); | ||||
|             break; | ||||
|           case 404: | ||||
|             setMissingRecordsText(t`Not found`); | ||||
|             break; | ||||
|           default: | ||||
|             setMissingRecordsText( | ||||
|               t`Unknown error` + ': ' + response.statusText | ||||
|             ); // TODO: Translate | ||||
|             break; | ||||
|         } | ||||
|  | ||||
|         return []; | ||||
|       }) | ||||
|       .catch(function (error) { | ||||
|         setMissingRecordsText(t`Error` + ': ' + error.message); | ||||
|         return []; | ||||
|       }); | ||||
|   }; | ||||
|  | ||||
|   const { data, isError, isFetching, isLoading, refetch } = useQuery( | ||||
|     [ | ||||
|       `table-${tableKey}`, | ||||
|       sortStatus.columnAccessor, | ||||
|       sortStatus.direction, | ||||
|       page, | ||||
|       activeFilters, | ||||
|       searchTerm | ||||
|     ], | ||||
|     fetchTableData, | ||||
|     { | ||||
|       refetchOnWindowFocus: false, | ||||
|       refetchOnMount: 'always' | ||||
|     } | ||||
|   ); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <FilterSelectModal | ||||
|         availableFilters={customFilters} | ||||
|         activeFilters={activeFilters} | ||||
|         opened={filterSelectOpen} | ||||
|         onCreateFilter={onFilterAdd} | ||||
|         onClose={() => setFilterSelectOpen(false)} | ||||
|       /> | ||||
|       <Stack> | ||||
|         <Group position="apart"> | ||||
|           <Group position="left" spacing={5}> | ||||
|             {customActionGroups.map((group: any, idx: number) => group)} | ||||
|             {barcodeActions.length > 0 && ( | ||||
|               <ButtonMenu | ||||
|                 icon={<IconBarcode />} | ||||
|                 label={t`Barcode actions`} | ||||
|                 tooltip={t`Barcode actions`} | ||||
|                 actions={barcodeActions} | ||||
|               /> | ||||
|             )} | ||||
|             {printingActions.length > 0 && ( | ||||
|               <ButtonMenu | ||||
|                 icon={<IconPrinter />} | ||||
|                 label={t`Print actions`} | ||||
|                 tooltip={t`Print actions`} | ||||
|                 actions={printingActions} | ||||
|               /> | ||||
|             )} | ||||
|             {enableDownload && ( | ||||
|               <DownloadAction downloadCallback={downloadData} /> | ||||
|             )} | ||||
|           </Group> | ||||
|           <Space /> | ||||
|           <Group position="right" spacing={5}> | ||||
|             {enableSearch && ( | ||||
|               <TableSearchInput | ||||
|                 searchCallback={(term: string) => setSearchTerm(term)} | ||||
|               /> | ||||
|             )} | ||||
|             {enableRefresh && ( | ||||
|               <ActionIcon> | ||||
|                 <Tooltip label={t`Refresh data`}> | ||||
|                   <IconRefresh onClick={() => refetch()} /> | ||||
|                 </Tooltip> | ||||
|               </ActionIcon> | ||||
|             )} | ||||
|             {hasSwitchableColumns && ( | ||||
|               <TableColumnSelect | ||||
|                 columns={dataColumns} | ||||
|                 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> | ||||
|             )} | ||||
|           </Group> | ||||
|         </Group> | ||||
|         {filtersVisible && ( | ||||
|           <FilterGroup | ||||
|             activeFilters={activeFilters} | ||||
|             onFilterAdd={() => setFilterSelectOpen(true)} | ||||
|             onFilterRemove={onFilterRemove} | ||||
|             onFilterClearAll={onFilterClearAll} | ||||
|           /> | ||||
|         )} | ||||
|         <DataTable | ||||
|           withBorder | ||||
|           striped | ||||
|           highlightOnHover | ||||
|           loaderVariant="dots" | ||||
|           idAccessor={'pk'} | ||||
|           minHeight={200} | ||||
|           totalRecords={data?.count ?? data?.length ?? 0} | ||||
|           recordsPerPage={pageSize} | ||||
|           page={page} | ||||
|           onPageChange={setPage} | ||||
|           sortStatus={sortStatus} | ||||
|           onSortStatusChange={handleSortStatusChange} | ||||
|           selectedRecords={enableSelection ? selectedRecords : undefined} | ||||
|           onSelectedRecordsChange={ | ||||
|             enableSelection ? onSelectedRecordsChange : undefined | ||||
|           } | ||||
|           fetching={isFetching} | ||||
|           noRecordsText={missingRecordsText} | ||||
|           records={data?.results ?? data ?? []} | ||||
|           columns={dataColumns} | ||||
|         /> | ||||
|       </Stack> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										31
									
								
								src/frontend/src/components/tables/Search.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/frontend/src/components/tables/Search.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| import { CloseButton, TextInput } from '@mantine/core'; | ||||
| import { useDebouncedValue } from '@mantine/hooks'; | ||||
| import { IconSearch } from '@tabler/icons-react'; | ||||
| import { useEffect, useState } from 'react'; | ||||
|  | ||||
| export function TableSearchInput({ | ||||
|   searchCallback | ||||
| }: { | ||||
|   searchCallback: (searchTerm: string) => void; | ||||
| }) { | ||||
|   const [value, setValue] = useState<string>(''); | ||||
|   const [searchText] = useDebouncedValue(value, 500); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     searchCallback(searchText); | ||||
|   }, [searchText]); | ||||
|  | ||||
|   return ( | ||||
|     <TextInput | ||||
|       value={value} | ||||
|       icon={<IconSearch />} | ||||
|       placeholder="Search" | ||||
|       onChange={(event) => setValue(event.target.value)} | ||||
|       rightSection={ | ||||
|         value.length > 0 ? ( | ||||
|           <CloseButton size="xs" onClick={(event) => setValue('')} /> | ||||
|         ) : null | ||||
|       } | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										135
									
								
								src/frontend/src/components/tables/build/BuildOrderTable.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								src/frontend/src/components/tables/build/BuildOrderTable.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | ||||
| import { t } from '@lingui/macro'; | ||||
| import { Progress } from '@mantine/core'; | ||||
| import { useMemo } from 'react'; | ||||
|  | ||||
| import { ThumbnailHoverCard } from '../../items/Thumbnail'; | ||||
| import { TableColumn } from '../Column'; | ||||
| import { TableFilter } from '../Filter'; | ||||
| import { InvenTreeTable } from '../InvenTreeTable'; | ||||
|  | ||||
| /** | ||||
|  * Construct a list of columns for the build order table | ||||
|  */ | ||||
| function buildOrderTableColumns(): TableColumn[] { | ||||
|   return [ | ||||
|     { | ||||
|       accessor: 'reference', | ||||
|       sortable: true, | ||||
|       title: t`Reference` | ||||
|       // TODO: Link to the build order detail page | ||||
|     }, | ||||
|     { | ||||
|       accessor: 'part', | ||||
|       sortable: true, | ||||
|       title: t`Part`, | ||||
|       render: (record: any) => { | ||||
|         let part = record.part_detail; | ||||
|         return ( | ||||
|           part && ( | ||||
|             <ThumbnailHoverCard | ||||
|               src={part.thumbnail || part.image} | ||||
|               text={part.full_name} | ||||
|               link="" | ||||
|             /> | ||||
|           ) | ||||
|         ); | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       accessor: 'title', | ||||
|       sortable: false, | ||||
|       title: t`Description`, | ||||
|       switchable: true | ||||
|     }, | ||||
|     { | ||||
|       accessor: 'project_code', | ||||
|       title: t`Project Code`, | ||||
|       sortable: true, | ||||
|       switchable: false, | ||||
|       hidden: true | ||||
|       // TODO: Hide this if project code is not enabled | ||||
|       // TODO: Custom render function here | ||||
|     }, | ||||
|     { | ||||
|       accessor: 'priority', | ||||
|       title: t`Priority`, | ||||
|       sortable: true, | ||||
|       switchable: true | ||||
|     }, | ||||
|     { | ||||
|       accessor: 'quantity', | ||||
|       sortable: true, | ||||
|       title: t`Quantity`, | ||||
|       switchable: true | ||||
|     }, | ||||
|     { | ||||
|       accessor: 'completed', | ||||
|       sortable: true, | ||||
|       title: t`Completed`, | ||||
|       render: (record: any) => { | ||||
|         let progress = | ||||
|           record.quantity <= 0 ? 0 : (100 * record.completed) / record.quantity; | ||||
|         return ( | ||||
|           <Progress | ||||
|             value={progress} | ||||
|             label={record.completed} | ||||
|             color={progress < 100 ? 'blue' : 'green'} | ||||
|             size="xl" | ||||
|             radius="xl" | ||||
|           /> | ||||
|         ); | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       accessor: 'status', | ||||
|       sortable: true, | ||||
|       title: t`Status`, | ||||
|       switchable: true | ||||
|       // TODO: Custom render function here (status label) | ||||
|     }, | ||||
|     { | ||||
|       accessor: 'creation_date', | ||||
|       sortable: true, | ||||
|       title: t`Created`, | ||||
|       switchable: true | ||||
|     } | ||||
|     // TODO: issued_by | ||||
|     // TODO: responsible | ||||
|     // TODO: target_date | ||||
|     // TODO: completion_date | ||||
|   ]; | ||||
| } | ||||
|  | ||||
| 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 | ||||
|   let tableParams = useMemo(() => buildOrderTableParams(params), [params]); | ||||
|   let tableColumns = useMemo(() => buildOrderTableColumns(), []); | ||||
|   let tableFilters = useMemo(() => buildOrderTableFilters(), []); | ||||
|  | ||||
|   tableParams.part_detail = true; | ||||
|  | ||||
|   return ( | ||||
|     <InvenTreeTable | ||||
|       url="build/" | ||||
|       enableDownload | ||||
|       tableKey="build-order-table" | ||||
|       params={tableParams} | ||||
|       columns={tableColumns} | ||||
|       customFilters={tableFilters} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										210
									
								
								src/frontend/src/components/tables/part/PartTable.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								src/frontend/src/components/tables/part/PartTable.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,210 @@ | ||||
| import { t } from '@lingui/macro'; | ||||
| import { Text } from '@mantine/core'; | ||||
| import { useMemo } from 'react'; | ||||
|  | ||||
| import { notYetImplemented } from '../../../functions/notifications'; | ||||
| import { shortenString } from '../../../functions/tables'; | ||||
| import { ThumbnailHoverCard } from '../../items/Thumbnail'; | ||||
| import { TableColumn } from '../Column'; | ||||
| import { TableFilter } from '../Filter'; | ||||
| import { InvenTreeTable } from '../InvenTreeTable'; | ||||
|  | ||||
| /** | ||||
|  * Construct a list of columns for the part table | ||||
|  */ | ||||
| function partTableColumns(): TableColumn[] { | ||||
|   return [ | ||||
|     { | ||||
|       accessor: 'name', | ||||
|       sortable: true, | ||||
|       title: t`Part`, | ||||
|       render: function (record: any) { | ||||
|         // TODO - Link to the part detail page | ||||
|         return ( | ||||
|           <ThumbnailHoverCard | ||||
|             src={record.thumbnail || record.image} | ||||
|             text={record.name} | ||||
|             link="" | ||||
|           /> | ||||
|         ); | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       accessor: 'IPN', | ||||
|       title: t`IPN`, | ||||
|       sortable: true, | ||||
|       switchable: true | ||||
|     }, | ||||
|     { | ||||
|       accessor: 'units', | ||||
|       sortable: true, | ||||
|       title: t`Units`, | ||||
|       switchable: true | ||||
|     }, | ||||
|     { | ||||
|       accessor: 'description', | ||||
|       title: t`Description`, | ||||
|       sortable: true, | ||||
|       switchable: true | ||||
|     }, | ||||
|     { | ||||
|       accessor: 'category', | ||||
|       title: t`Category`, | ||||
|       sortable: true, | ||||
|       render: function (record: any) { | ||||
|         // TODO: Link to the category detail page | ||||
|         return shortenString({ | ||||
|           str: record.category_detail.pathstring | ||||
|         }); | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       accessor: 'total_in_stock', | ||||
|       title: t`Stock`, | ||||
|       sortable: true, | ||||
|       switchable: true | ||||
|     }, | ||||
|     { | ||||
|       accessor: 'price_range', | ||||
|       title: t`Price Range`, | ||||
|       sortable: false, | ||||
|       switchable: true, | ||||
|       render: function (record: any) { | ||||
|         // TODO: Render price range | ||||
|         return '-- price --'; | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       accessor: 'link', | ||||
|       title: t`Link`, | ||||
|       switchable: true | ||||
|     } | ||||
|   ]; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Construct a set of filters for the part table | ||||
|  */ | ||||
| function partTableFilters(): TableFilter[] { | ||||
|   return [ | ||||
|     { | ||||
|       name: 'active', | ||||
|       label: t`Active`, | ||||
|       description: t`Filter by part active status`, | ||||
|       type: 'boolean' | ||||
|     }, | ||||
|     { | ||||
|       name: 'assembly', | ||||
|       label: t`Assembly`, | ||||
|       description: t`Filter by assembly attribute`, | ||||
|       type: 'boolean' | ||||
|     }, | ||||
|     { | ||||
|       name: 'cascade', | ||||
|       label: t`Include Subcategories`, | ||||
|       description: t`Include parts in subcategories`, | ||||
|       type: 'boolean' | ||||
|     }, | ||||
|     { | ||||
|       name: 'component', | ||||
|       label: t`Component`, | ||||
|       description: t`Filter by component attribute`, | ||||
|       type: 'boolean' | ||||
|     }, | ||||
|     { | ||||
|       name: 'trackable', | ||||
|       label: t`Trackable`, | ||||
|       description: t`Filter by trackable attribute`, | ||||
|       type: 'boolean' | ||||
|     }, | ||||
|     { | ||||
|       name: 'has_units', | ||||
|       label: t`Has Units`, | ||||
|       description: t`Filter by parts which have units`, | ||||
|       type: 'boolean' | ||||
|     }, | ||||
|     { | ||||
|       name: 'has_ipn', | ||||
|       label: t`Has IPN`, | ||||
|       description: t`Filter by parts which have an internal part number`, | ||||
|       type: 'boolean' | ||||
|     }, | ||||
|     { | ||||
|       name: 'has_stock', | ||||
|       label: t`Has Stock`, | ||||
|       description: t`Filter by parts which have stock`, | ||||
|       type: 'boolean' | ||||
|     }, | ||||
|     { | ||||
|       name: 'low_stock', | ||||
|       label: t`Low Stock`, | ||||
|       description: t`Filter by parts which have low stock`, | ||||
|       type: 'boolean' | ||||
|     }, | ||||
|     { | ||||
|       name: 'purchaseable', | ||||
|       label: t`Purchaseable`, | ||||
|       description: t`Filter by parts which are purchaseable`, | ||||
|       type: 'boolean' | ||||
|     }, | ||||
|     { | ||||
|       name: 'salable', | ||||
|       label: t`Salable`, | ||||
|       description: t`Filter by parts which are salable`, | ||||
|       type: 'boolean' | ||||
|     }, | ||||
|     { | ||||
|       name: 'virtual', | ||||
|       label: t`Virtual`, | ||||
|       description: t`Filter by parts which are virtual`, | ||||
|       type: 'choice', | ||||
|       choices: [ | ||||
|         { value: 'true', label: t`Virtual` }, | ||||
|         { value: 'false', label: t`Not Virtual` } | ||||
|       ] | ||||
|     } | ||||
|     // unallocated_stock | ||||
|     // starred | ||||
|     // stocktake | ||||
|     // is_template | ||||
|     // virtual | ||||
|     // has_pricing | ||||
|     // TODO: Any others from table_filters.js? | ||||
|   ]; | ||||
| } | ||||
|  | ||||
| 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), []); | ||||
|   let tableColumns = useMemo(() => partTableColumns(), []); | ||||
|   let tableFilters = useMemo(() => partTableFilters(), []); | ||||
|  | ||||
|   // Add required query parameters | ||||
|   tableParams.category_detail = true; | ||||
|  | ||||
|   return ( | ||||
|     <InvenTreeTable | ||||
|       url="part/" | ||||
|       enableDownload | ||||
|       tableKey="part-table" | ||||
|       printingActions={[ | ||||
|         <Text onClick={notYetImplemented}>Hello</Text>, | ||||
|         <Text onClick={notYetImplemented}>World</Text> | ||||
|       ]} | ||||
|       params={tableParams} | ||||
|       columns={tableColumns} | ||||
|       customFilters={tableFilters} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										155
									
								
								src/frontend/src/components/tables/stock/StockItemTable.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								src/frontend/src/components/tables/stock/StockItemTable.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,155 @@ | ||||
| import { t } from '@lingui/macro'; | ||||
| import { Group } from '@mantine/core'; | ||||
| import { IconEdit, IconTrash } from '@tabler/icons-react'; | ||||
| import { useEffect, useMemo, useState } from 'react'; | ||||
|  | ||||
| import { notYetImplemented } from '../../../functions/notifications'; | ||||
| import { ActionButton } from '../../items/ActionButton'; | ||||
| import { ThumbnailHoverCard } from '../../items/Thumbnail'; | ||||
| import { TableColumn } from '../Column'; | ||||
| import { TableFilter } from '../Filter'; | ||||
| import { InvenTreeTable } from './../InvenTreeTable'; | ||||
|  | ||||
| /** | ||||
|  * Construct a list of columns for the stock item table | ||||
|  */ | ||||
| function stockItemTableColumns(): TableColumn[] { | ||||
|   return [ | ||||
|     { | ||||
|       accessor: 'part', | ||||
|       sortable: true, | ||||
|       title: t`Part`, | ||||
|       render: function (record: any) { | ||||
|         let part = record.part_detail; | ||||
|         return ( | ||||
|           <ThumbnailHoverCard | ||||
|             src={part.thumbnail || part.image} | ||||
|             text={part.name} | ||||
|             link="" | ||||
|           /> | ||||
|         ); | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       accessor: 'part_detail.description', | ||||
|       sortable: false, | ||||
|       switchable: true, | ||||
|       title: t`Description` | ||||
|     }, | ||||
|     { | ||||
|       accessor: 'quantity', | ||||
|       sortable: true, | ||||
|       title: t`Stock` | ||||
|       // TODO: Custom renderer for stock quantity | ||||
|     }, | ||||
|     { | ||||
|       accessor: 'status', | ||||
|       sortable: true, | ||||
|       switchable: true, | ||||
|       filter: true, | ||||
|       title: t`Status` | ||||
|       // TODO: Custom renderer for stock status label | ||||
|     }, | ||||
|     { | ||||
|       accessor: 'batch', | ||||
|       sortable: true, | ||||
|       switchable: true, | ||||
|       title: t`Batch` | ||||
|     }, | ||||
|     { | ||||
|       accessor: 'location', | ||||
|       sortable: true, | ||||
|       switchable: true, | ||||
|       title: t`Location`, | ||||
|       render: function (record: any) { | ||||
|         // TODO: Custom renderer for location | ||||
|         return record.location; | ||||
|       } | ||||
|     }, | ||||
|     // TODO: stocktake column | ||||
|     // TODO: expiry date | ||||
|     // TODO: last updated | ||||
|     // TODO: purchase order | ||||
|     // TODO: Supplier part | ||||
|     // TODO: purchase price | ||||
|     // TODO: stock value | ||||
|     // TODO: packaging | ||||
|     // TODO: notes | ||||
|     { | ||||
|       accessor: 'actions', | ||||
|       title: t`Actions`, | ||||
|       sortable: false, | ||||
|       render: function (record: any) { | ||||
|         return ( | ||||
|           <Group position="right" spacing={5} noWrap={true}> | ||||
|             {/* {EditButton(setEditing, editing)} */} | ||||
|             {/* {DeleteButton()} */} | ||||
|             <ActionButton | ||||
|               color="green" | ||||
|               icon={<IconEdit />} | ||||
|               tooltip="Edit stock item" | ||||
|               onClick={() => notYetImplemented()} | ||||
|             /> | ||||
|             <ActionButton | ||||
|               color="red" | ||||
|               tooltip="Delete stock item" | ||||
|               icon={<IconTrash />} | ||||
|               onClick={() => notYetImplemented()} | ||||
|             /> | ||||
|           </Group> | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|   ]; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Return a set of parameters for the stock item table | ||||
|  */ | ||||
| function stockItemTableParams(params: any): any { | ||||
|   return { | ||||
|     ...params, | ||||
|     part_detail: true, | ||||
|     location_detail: true | ||||
|   }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Construct a list of available filters for the stock item table | ||||
|  */ | ||||
| function stockItemTableFilters(): TableFilter[] { | ||||
|   return [ | ||||
|     { | ||||
|       name: 'test_filter', | ||||
|       label: t`Test Filter`, | ||||
|       description: t`This is a test filter`, | ||||
|       type: 'choice', | ||||
|       choiceFunction: () => [ | ||||
|         { value: '1', label: 'One' }, | ||||
|         { value: '2', label: 'Two' }, | ||||
|         { value: '3', label: 'Three' } | ||||
|       ] | ||||
|     } | ||||
|   ]; | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * Load a table of stock items | ||||
|  */ | ||||
| export function StockItemTable({ params = {} }: { params?: any }) { | ||||
|   let tableParams = useMemo(() => stockItemTableParams(params), []); | ||||
|   let tableColumns = useMemo(() => stockItemTableColumns(), []); | ||||
|   let tableFilters = useMemo(() => stockItemTableFilters(), []); | ||||
|  | ||||
|   return ( | ||||
|     <InvenTreeTable | ||||
|       url="stock/" | ||||
|       tableKey="stock-table" | ||||
|       enableDownload | ||||
|       enableSelection | ||||
|       params={tableParams} | ||||
|       columns={tableColumns} | ||||
|       customFilters={tableFilters} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| @@ -21,7 +21,10 @@ export const footerLinks = [ | ||||
| ]; | ||||
| export const navTabs = [ | ||||
|   { text: <Trans>Home</Trans>, name: 'home' }, | ||||
|   { text: <Trans>Dashboard</Trans>, name: 'dashboard' } | ||||
|   { text: <Trans>Dashboard</Trans>, name: 'dashboard' }, | ||||
|   { text: <Trans>Parts</Trans>, name: 'parts' }, | ||||
|   { text: <Trans>Stock</Trans>, name: 'stock' }, | ||||
|   { text: <Trans>Build</Trans>, name: 'build' } | ||||
| ]; | ||||
|  | ||||
| export const docLinks = { | ||||
|   | ||||
							
								
								
									
										13
									
								
								src/frontend/src/functions/notifications.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/frontend/src/functions/notifications.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| import { t } from '@lingui/macro'; | ||||
| import { notifications } from '@mantine/notifications'; | ||||
|  | ||||
| /** | ||||
|  * Show a notification that the feature is not yet implemented | ||||
|  */ | ||||
| export function notYetImplemented() { | ||||
|   notifications.show({ | ||||
|     title: t`Not implemented`, | ||||
|     message: t`This feature is not yet implemented`, | ||||
|     color: 'red' | ||||
|   }); | ||||
| } | ||||
							
								
								
									
										25
									
								
								src/frontend/src/functions/tables.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/frontend/src/functions/tables.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| /** | ||||
|  * Reduce an input string to a given length, adding an ellipsis if necessary | ||||
|  * @param str - String to shorten | ||||
|  * @param len - Length to shorten to | ||||
|  */ | ||||
| export function shortenString({ | ||||
|   str, | ||||
|   len = 100 | ||||
| }: { | ||||
|   str: string; | ||||
|   len?: number; | ||||
| }) { | ||||
|   // Ensure that the string is a string | ||||
|   str = str.toString(); | ||||
|  | ||||
|   // If the string is already short enough, return it | ||||
|   if (str.length <= len) { | ||||
|     return str; | ||||
|   } | ||||
|  | ||||
|   // Otherwise, shorten it | ||||
|   let N = Math.floor(len / 2 - 1); | ||||
|  | ||||
|   return str.slice(0, N) + '...' + str.slice(-N); | ||||
| } | ||||
							
								
								
									
										20
									
								
								src/frontend/src/pages/Index/Build.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/frontend/src/pages/Index/Build.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| import { Trans } from '@lingui/macro'; | ||||
| import { Group } from '@mantine/core'; | ||||
|  | ||||
| import { PlaceholderPill } from '../../components/items/Placeholder'; | ||||
| import { StylishText } from '../../components/items/StylishText'; | ||||
| import { BuildOrderTable } from '../../components/tables/build/BuildOrderTable'; | ||||
|  | ||||
| export default function Build() { | ||||
|   return ( | ||||
|     <> | ||||
|       <Group> | ||||
|         <StylishText> | ||||
|           <Trans>Build Orders</Trans> | ||||
|         </StylishText> | ||||
|         <PlaceholderPill /> | ||||
|       </Group> | ||||
|       <BuildOrderTable /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										20
									
								
								src/frontend/src/pages/Index/Part.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/frontend/src/pages/Index/Part.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| import { Trans } from '@lingui/macro'; | ||||
| import { Group } from '@mantine/core'; | ||||
|  | ||||
| import { PlaceholderPill } from '../../components/items/Placeholder'; | ||||
| import { StylishText } from '../../components/items/StylishText'; | ||||
| import { PartListTable } from '../../components/tables/part/PartTable'; | ||||
|  | ||||
| export default function Part() { | ||||
|   return ( | ||||
|     <> | ||||
|       <Group> | ||||
|         <StylishText> | ||||
|           <Trans>Parts</Trans> | ||||
|         </StylishText> | ||||
|         <PlaceholderPill /> | ||||
|       </Group> | ||||
|       <PartListTable /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										20
									
								
								src/frontend/src/pages/Index/Stock.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/frontend/src/pages/Index/Stock.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| import { Trans } from '@lingui/macro'; | ||||
| import { Group } from '@mantine/core'; | ||||
|  | ||||
| import { PlaceholderPill } from '../../components/items/Placeholder'; | ||||
| import { StylishText } from '../../components/items/StylishText'; | ||||
| import { StockItemTable } from '../../components/tables/stock/StockItemTable'; | ||||
|  | ||||
| export default function Stock() { | ||||
|   return ( | ||||
|     <> | ||||
|       <Group> | ||||
|         <StylishText> | ||||
|           <Trans>Stock Items</Trans> | ||||
|         </StylishText> | ||||
|         <PlaceholderPill /> | ||||
|       </Group> | ||||
|       <StockItemTable /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| @@ -8,6 +8,10 @@ export const LayoutComponent = Loadable( | ||||
|   lazy(() => import('./components/nav/Layout')) | ||||
| ); | ||||
| export const Home = Loadable(lazy(() => import('./pages/Index/Home'))); | ||||
| export const Parts = Loadable(lazy(() => import('./pages/Index/Part'))); | ||||
| export const Stock = Loadable(lazy(() => import('./pages/Index/Stock'))); | ||||
| export const Build = Loadable(lazy(() => import('./pages/Index/Build'))); | ||||
|  | ||||
| export const Dashboard = Loadable( | ||||
|   lazy(() => import('./pages/Index/Dashboard')) | ||||
| ); | ||||
| @@ -48,6 +52,18 @@ export const router = createBrowserRouter( | ||||
|           path: 'dashboard/', | ||||
|           element: <Dashboard /> | ||||
|         }, | ||||
|         { | ||||
|           path: 'parts/', | ||||
|           element: <Parts /> | ||||
|         }, | ||||
|         { | ||||
|           path: 'stock/', | ||||
|           element: <Stock /> | ||||
|         }, | ||||
|         { | ||||
|           path: 'build/', | ||||
|           element: <Build /> | ||||
|         }, | ||||
|         { | ||||
|           path: '/profile/:tabValue', | ||||
|           element: <Profile /> | ||||
|   | ||||
| @@ -14,5 +14,10 @@ export default defineConfig({ | ||||
|   build: { | ||||
|     manifest: true, | ||||
|     outDir: '../../InvenTree/web/static/web' | ||||
|   }, | ||||
|   server: { | ||||
|     watch: { | ||||
|       usePolling: true | ||||
|     } | ||||
|   } | ||||
| }); | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user