mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-04 07:05:41 +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;
 | 
					  url: string;
 | 
				
			||||||
  pk?: number;
 | 
					  pk?: number;
 | 
				
			||||||
  title: string;
 | 
					  title: string;
 | 
				
			||||||
  fields: ApiFormFieldSet;
 | 
					  fields?: ApiFormFieldSet;
 | 
				
			||||||
  cancelText?: string;
 | 
					  cancelText?: string;
 | 
				
			||||||
  submitText?: string;
 | 
					  submitText?: string;
 | 
				
			||||||
  submitColor?: string;
 | 
					  submitColor?: string;
 | 
				
			||||||
@@ -118,7 +118,7 @@ export function ApiForm({
 | 
				
			|||||||
        .get(url)
 | 
					        .get(url)
 | 
				
			||||||
        .then((response) => {
 | 
					        .then((response) => {
 | 
				
			||||||
          // Update form values, but only for the fields specified for the form
 | 
					          // 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) {
 | 
					            if (fieldName in response.data) {
 | 
				
			||||||
              form.setValues({
 | 
					              form.setValues({
 | 
				
			||||||
                [fieldName]: response.data[fieldName]
 | 
					                [fieldName]: response.data[fieldName]
 | 
				
			||||||
@@ -137,7 +137,7 @@ export function ApiForm({
 | 
				
			|||||||
  // Fetch initial data on form load
 | 
					  // Fetch initial data on form load
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    // Provide initial form data
 | 
					    // Provide initial form data
 | 
				
			||||||
    Object.entries(props.fields).forEach(([fieldName, field]) => {
 | 
					    Object.entries(props.fields ?? {}).forEach(([fieldName, field]) => {
 | 
				
			||||||
      if (field.value !== undefined) {
 | 
					      if (field.value !== undefined) {
 | 
				
			||||||
        form.setValues({
 | 
					        form.setValues({
 | 
				
			||||||
          [fieldName]: field.value
 | 
					          [fieldName]: field.value
 | 
				
			||||||
@@ -272,7 +272,7 @@ export function ApiForm({
 | 
				
			|||||||
        {preFormElement}
 | 
					        {preFormElement}
 | 
				
			||||||
        <ScrollArea>
 | 
					        <ScrollArea>
 | 
				
			||||||
          <Stack spacing="xs">
 | 
					          <Stack spacing="xs">
 | 
				
			||||||
            {Object.entries(props.fields).map(
 | 
					            {Object.entries(props.fields ?? {}).map(
 | 
				
			||||||
              ([fieldName, field]) =>
 | 
					              ([fieldName, field]) =>
 | 
				
			||||||
                !field.hidden && (
 | 
					                !field.hidden && (
 | 
				
			||||||
                  <ApiFormField
 | 
					                  <ApiFormField
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,7 @@ import { api } from '../../App';
 | 
				
			|||||||
export function Thumbnail({
 | 
					export function Thumbnail({
 | 
				
			||||||
  src,
 | 
					  src,
 | 
				
			||||||
  alt = t`Thumbnail`,
 | 
					  alt = t`Thumbnail`,
 | 
				
			||||||
  size = 24
 | 
					  size = 20
 | 
				
			||||||
}: {
 | 
					}: {
 | 
				
			||||||
  src: string;
 | 
					  src: string;
 | 
				
			||||||
  alt?: string;
 | 
					  alt?: string;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -174,7 +174,7 @@ export function AttachmentTable({
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function customActionGroups(): ReactNode[] {
 | 
					  const customActionGroups: ReactNode[] = useMemo(() => {
 | 
				
			||||||
    let actions = [];
 | 
					    let actions = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (allowEdit) {
 | 
					    if (allowEdit) {
 | 
				
			||||||
@@ -218,7 +218,7 @@ export function AttachmentTable({
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return actions;
 | 
					    return actions;
 | 
				
			||||||
  }
 | 
					  }, [allowEdit]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Stack spacing="xs">
 | 
					    <Stack spacing="xs">
 | 
				
			||||||
@@ -229,7 +229,7 @@ export function AttachmentTable({
 | 
				
			|||||||
        params={{
 | 
					        params={{
 | 
				
			||||||
          [model]: pk
 | 
					          [model]: pk
 | 
				
			||||||
        }}
 | 
					        }}
 | 
				
			||||||
        customActionGroups={customActionGroups()}
 | 
					        customActionGroups={customActionGroups}
 | 
				
			||||||
        columns={tableColumns}
 | 
					        columns={tableColumns}
 | 
				
			||||||
        rowActions={allowEdit && allowDelete ? rowActions : undefined}
 | 
					        rowActions={allowEdit && allowDelete ? rowActions : undefined}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -45,7 +45,7 @@ export function RowActions({
 | 
				
			|||||||
              icon={action.icon}
 | 
					              icon={action.icon}
 | 
				
			||||||
              title={action.tooltip || action.title}
 | 
					              title={action.tooltip || action.title}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <Text size="sm" color={action.color}>
 | 
					              <Text size="xs" color={action.color}>
 | 
				
			||||||
                {action.title}
 | 
					                {action.title}
 | 
				
			||||||
              </Text>
 | 
					              </Text>
 | 
				
			||||||
            </Menu.Item>
 | 
					            </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,
 | 
					    ...props,
 | 
				
			||||||
    method: 'DELETE',
 | 
					    method: 'DELETE',
 | 
				
			||||||
    submitText: t`Delete`,
 | 
					    submitText: t`Delete`,
 | 
				
			||||||
    submitColor: 'red'
 | 
					    submitColor: 'red',
 | 
				
			||||||
 | 
					    fields: {}
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  openModalApiForm(deleteProps);
 | 
					  openModalApiForm(deleteProps);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,14 +3,11 @@ import {
 | 
				
			|||||||
  Button,
 | 
					  Button,
 | 
				
			||||||
  Group,
 | 
					  Group,
 | 
				
			||||||
  LoadingOverlay,
 | 
					  LoadingOverlay,
 | 
				
			||||||
  Skeleton,
 | 
					 | 
				
			||||||
  Space,
 | 
					  Space,
 | 
				
			||||||
  Stack,
 | 
					  Stack,
 | 
				
			||||||
  Tabs,
 | 
					 | 
				
			||||||
  Text
 | 
					  Text
 | 
				
			||||||
} from '@mantine/core';
 | 
					} from '@mantine/core';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  IconBox,
 | 
					 | 
				
			||||||
  IconBuilding,
 | 
					  IconBuilding,
 | 
				
			||||||
  IconCurrencyDollar,
 | 
					  IconCurrencyDollar,
 | 
				
			||||||
  IconInfoCircle,
 | 
					  IconInfoCircle,
 | 
				
			||||||
@@ -34,6 +31,7 @@ import { useNavigate, useParams } from 'react-router-dom';
 | 
				
			|||||||
import { api } from '../../App';
 | 
					import { api } from '../../App';
 | 
				
			||||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
 | 
					import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
 | 
				
			||||||
import { AttachmentTable } from '../../components/tables/AttachmentTable';
 | 
					import { AttachmentTable } from '../../components/tables/AttachmentTable';
 | 
				
			||||||
 | 
					import { RelatedPartTable } from '../../components/tables/part/RelatedPartTable';
 | 
				
			||||||
import { StockItemTable } from '../../components/tables/stock/StockItemTable';
 | 
					import { StockItemTable } from '../../components/tables/stock/StockItemTable';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  MarkdownEditor,
 | 
					  MarkdownEditor,
 | 
				
			||||||
@@ -128,7 +126,7 @@ export default function PartDetail() {
 | 
				
			|||||||
        name: 'related_parts',
 | 
					        name: 'related_parts',
 | 
				
			||||||
        label: t`Related Parts`,
 | 
					        label: t`Related Parts`,
 | 
				
			||||||
        icon: <IconLayersLinked size="18" />,
 | 
					        icon: <IconLayersLinked size="18" />,
 | 
				
			||||||
        content: <Text>part related parts go here</Text>
 | 
					        content: partRelatedTab()
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        name: 'attachments',
 | 
					        name: 'attachments',
 | 
				
			||||||
@@ -171,6 +169,9 @@ export default function PartDetail() {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function partRelatedTab(): React.ReactNode {
 | 
				
			||||||
 | 
					    return <RelatedPartTable partId={part.pk ?? -1} />;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
  function partNotesTab(): React.ReactNode {
 | 
					  function partNotesTab(): React.ReactNode {
 | 
				
			||||||
    // TODO: Set edit permission based on user permissions
 | 
					    // TODO: Set edit permission based on user permissions
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user