mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 21:25:42 +00:00 
			
		
		
		
	[PUI] Add simple "related parts" table (#5530)
* Add simple "related parts" table - Still needs method to create a new related part - Should allow user to click through to related parts - Need to implement the "delete related part" functionality * Fix image preview * Add action to delete part relationship * Implement function to add new related part * Implement simple "click through" for the related parts table - Will need to be improved later on * fix
This commit is contained in:
		| @@ -48,7 +48,7 @@ export interface ApiFormProps { | ||||
|   url: string; | ||||
|   pk?: number; | ||||
|   title: string; | ||||
|   fields: ApiFormFieldSet; | ||||
|   fields?: ApiFormFieldSet; | ||||
|   cancelText?: string; | ||||
|   submitText?: string; | ||||
|   submitColor?: string; | ||||
| @@ -118,7 +118,7 @@ export function ApiForm({ | ||||
|         .get(url) | ||||
|         .then((response) => { | ||||
|           // Update form values, but only for the fields specified for the form | ||||
|           Object.keys(props.fields).forEach((fieldName) => { | ||||
|           Object.keys(props.fields ?? {}).forEach((fieldName) => { | ||||
|             if (fieldName in response.data) { | ||||
|               form.setValues({ | ||||
|                 [fieldName]: response.data[fieldName] | ||||
| @@ -137,7 +137,7 @@ export function ApiForm({ | ||||
|   // Fetch initial data on form load | ||||
|   useEffect(() => { | ||||
|     // Provide initial form data | ||||
|     Object.entries(props.fields).forEach(([fieldName, field]) => { | ||||
|     Object.entries(props.fields ?? {}).forEach(([fieldName, field]) => { | ||||
|       if (field.value !== undefined) { | ||||
|         form.setValues({ | ||||
|           [fieldName]: field.value | ||||
| @@ -272,7 +272,7 @@ export function ApiForm({ | ||||
|         {preFormElement} | ||||
|         <ScrollArea> | ||||
|           <Stack spacing="xs"> | ||||
|             {Object.entries(props.fields).map( | ||||
|             {Object.entries(props.fields ?? {}).map( | ||||
|               ([fieldName, field]) => | ||||
|                 !field.hidden && ( | ||||
|                   <ApiFormField | ||||
|   | ||||
| @@ -8,7 +8,7 @@ import { api } from '../../App'; | ||||
| export function Thumbnail({ | ||||
|   src, | ||||
|   alt = t`Thumbnail`, | ||||
|   size = 24 | ||||
|   size = 20 | ||||
| }: { | ||||
|   src: string; | ||||
|   alt?: string; | ||||
|   | ||||
| @@ -174,7 +174,7 @@ export function AttachmentTable({ | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   function customActionGroups(): ReactNode[] { | ||||
|   const customActionGroups: ReactNode[] = useMemo(() => { | ||||
|     let actions = []; | ||||
|  | ||||
|     if (allowEdit) { | ||||
| @@ -218,7 +218,7 @@ export function AttachmentTable({ | ||||
|     } | ||||
|  | ||||
|     return actions; | ||||
|   } | ||||
|   }, [allowEdit]); | ||||
|  | ||||
|   return ( | ||||
|     <Stack spacing="xs"> | ||||
| @@ -229,7 +229,7 @@ export function AttachmentTable({ | ||||
|         params={{ | ||||
|           [model]: pk | ||||
|         }} | ||||
|         customActionGroups={customActionGroups()} | ||||
|         customActionGroups={customActionGroups} | ||||
|         columns={tableColumns} | ||||
|         rowActions={allowEdit && allowDelete ? rowActions : undefined} | ||||
|       /> | ||||
|   | ||||
| @@ -45,7 +45,7 @@ export function RowActions({ | ||||
|               icon={action.icon} | ||||
|               title={action.tooltip || action.title} | ||||
|             > | ||||
|               <Text size="sm" color={action.color}> | ||||
|               <Text size="xs" color={action.color}> | ||||
|                 {action.title} | ||||
|               </Text> | ||||
|             </Menu.Item> | ||||
|   | ||||
							
								
								
									
										129
									
								
								src/frontend/src/components/tables/part/RelatedPartTable.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								src/frontend/src/components/tables/part/RelatedPartTable.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,129 @@ | ||||
| import { t } from '@lingui/macro'; | ||||
| import { ActionIcon, Group, Text, Tooltip } from '@mantine/core'; | ||||
| import { IconLayersLinked } from '@tabler/icons-react'; | ||||
| import { ReactNode, useCallback, useMemo } from 'react'; | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
|  | ||||
| import { openCreateApiForm, openDeleteApiForm } from '../../../functions/forms'; | ||||
| import { useTableRefresh } from '../../../hooks/TableRefresh'; | ||||
| import { Thumbnail } from '../../items/Thumbnail'; | ||||
| import { TableColumn } from '../Column'; | ||||
| import { InvenTreeTable } from '../InvenTreeTable'; | ||||
|  | ||||
| export function RelatedPartTable({ partId }: { partId: number }): ReactNode { | ||||
|   const { refreshId, refreshTable } = useTableRefresh(); | ||||
|  | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   // Construct table columns for this table | ||||
|   const tableColumns: TableColumn[] = useMemo(() => { | ||||
|     function getPart(record: any) { | ||||
|       if (record.part_1 == partId) { | ||||
|         return record.part_2_detail; | ||||
|       } else { | ||||
|         return record.part_1_detail; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return [ | ||||
|       { | ||||
|         accessor: 'part', | ||||
|         title: t`Part`, | ||||
|         noWrap: true, | ||||
|         render: (record: any) => { | ||||
|           let part = getPart(record); | ||||
|           return ( | ||||
|             <Group | ||||
|               onClick={() => { | ||||
|                 navigate(`/part/${part.pk}/`); | ||||
|               }} | ||||
|             > | ||||
|               <Thumbnail src={part.thumbnail || part.image} /> | ||||
|               <Text>{part.name}</Text> | ||||
|             </Group> | ||||
|           ); | ||||
|         } | ||||
|       }, | ||||
|       { | ||||
|         accessor: 'description', | ||||
|         title: t`Description`, | ||||
|         ellipsis: true, | ||||
|         render: (record: any) => { | ||||
|           return getPart(record).description; | ||||
|         } | ||||
|       } | ||||
|     ]; | ||||
|   }, []); | ||||
|  | ||||
|   const addRelatedPart = useCallback(() => { | ||||
|     openCreateApiForm({ | ||||
|       name: 'add-related-part', | ||||
|       title: t`Add Related Part`, | ||||
|       url: '/part/related/', | ||||
|       fields: { | ||||
|         part_1: { | ||||
|           hidden: true, | ||||
|           value: partId | ||||
|         }, | ||||
|         part_2: { | ||||
|           label: t`Related Part` | ||||
|         } | ||||
|       }, | ||||
|       successMessage: t`Related part added`, | ||||
|       onFormSuccess: refreshTable | ||||
|     }); | ||||
|   }, []); | ||||
|  | ||||
|   const customActions: ReactNode[] = useMemo(() => { | ||||
|     // TODO: Hide if user does not have permission to edit parts | ||||
|     let actions = []; | ||||
|  | ||||
|     actions.push( | ||||
|       <Tooltip label={t`Add related part`}> | ||||
|         <ActionIcon radius="sm" onClick={addRelatedPart}> | ||||
|           <IconLayersLinked /> | ||||
|         </ActionIcon> | ||||
|       </Tooltip> | ||||
|     ); | ||||
|  | ||||
|     return actions; | ||||
|   }, []); | ||||
|  | ||||
|   // Generate row actions | ||||
|   // TODO: Hide if user does not have permission to edit parts | ||||
|   const rowActions = useCallback((record: any) => { | ||||
|     return [ | ||||
|       { | ||||
|         title: t`Delete`, | ||||
|         color: 'red', | ||||
|         onClick: () => { | ||||
|           openDeleteApiForm({ | ||||
|             name: 'delete-related-part', | ||||
|             url: '/part/related/', | ||||
|             pk: record.pk, | ||||
|             title: t`Delete Related Part`, | ||||
|             successMessage: t`Related part deleted`, | ||||
|             preFormContent: ( | ||||
|               <Text>{t`Are you sure you want to remove this relationship?`}</Text> | ||||
|             ), | ||||
|             onFormSuccess: refreshTable | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|     ]; | ||||
|   }, []); | ||||
|  | ||||
|   return ( | ||||
|     <InvenTreeTable | ||||
|       url="/part/related/" | ||||
|       tableKey="related-part-table" | ||||
|       refreshId={refreshId} | ||||
|       params={{ | ||||
|         part: partId | ||||
|       }} | ||||
|       rowActions={rowActions} | ||||
|       columns={tableColumns} | ||||
|       customActionGroups={customActions} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| @@ -175,7 +175,8 @@ export function openDeleteApiForm(props: ApiFormProps) { | ||||
|     ...props, | ||||
|     method: 'DELETE', | ||||
|     submitText: t`Delete`, | ||||
|     submitColor: 'red' | ||||
|     submitColor: 'red', | ||||
|     fields: {} | ||||
|   }; | ||||
|  | ||||
|   openModalApiForm(deleteProps); | ||||
|   | ||||
| @@ -3,14 +3,11 @@ import { | ||||
|   Button, | ||||
|   Group, | ||||
|   LoadingOverlay, | ||||
|   Skeleton, | ||||
|   Space, | ||||
|   Stack, | ||||
|   Tabs, | ||||
|   Text | ||||
| } from '@mantine/core'; | ||||
| import { | ||||
|   IconBox, | ||||
|   IconBuilding, | ||||
|   IconCurrencyDollar, | ||||
|   IconInfoCircle, | ||||
| @@ -34,6 +31,7 @@ import { useNavigate, useParams } from 'react-router-dom'; | ||||
| import { api } from '../../App'; | ||||
| import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; | ||||
| import { AttachmentTable } from '../../components/tables/AttachmentTable'; | ||||
| import { RelatedPartTable } from '../../components/tables/part/RelatedPartTable'; | ||||
| import { StockItemTable } from '../../components/tables/stock/StockItemTable'; | ||||
| import { | ||||
|   MarkdownEditor, | ||||
| @@ -128,7 +126,7 @@ export default function PartDetail() { | ||||
|         name: 'related_parts', | ||||
|         label: t`Related Parts`, | ||||
|         icon: <IconLayersLinked size="18" />, | ||||
|         content: <Text>part related parts go here</Text> | ||||
|         content: partRelatedTab() | ||||
|       }, | ||||
|       { | ||||
|         name: 'attachments', | ||||
| @@ -171,6 +169,9 @@ export default function PartDetail() { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   function partRelatedTab(): React.ReactNode { | ||||
|     return <RelatedPartTable partId={part.pk ?? -1} />; | ||||
|   } | ||||
|   function partNotesTab(): React.ReactNode { | ||||
|     // TODO: Set edit permission based on user permissions | ||||
|     return ( | ||||
|   | ||||
		Reference in New Issue
	
	Block a user