diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index cf610785f2..46c926f7e2 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -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} - {Object.entries(props.fields).map( + {Object.entries(props.fields ?? {}).map( ([fieldName, field]) => !field.hidden && ( { let actions = []; if (allowEdit) { @@ -218,7 +218,7 @@ export function AttachmentTable({ } return actions; - } + }, [allowEdit]); return ( @@ -229,7 +229,7 @@ export function AttachmentTable({ params={{ [model]: pk }} - customActionGroups={customActionGroups()} + customActionGroups={customActionGroups} columns={tableColumns} rowActions={allowEdit && allowDelete ? rowActions : undefined} /> diff --git a/src/frontend/src/components/tables/RowActions.tsx b/src/frontend/src/components/tables/RowActions.tsx index bfc83c626f..b3080747f0 100644 --- a/src/frontend/src/components/tables/RowActions.tsx +++ b/src/frontend/src/components/tables/RowActions.tsx @@ -45,7 +45,7 @@ export function RowActions({ icon={action.icon} title={action.tooltip || action.title} > - + {action.title} diff --git a/src/frontend/src/components/tables/part/RelatedPartTable.tsx b/src/frontend/src/components/tables/part/RelatedPartTable.tsx new file mode 100644 index 0000000000..c1831cff03 --- /dev/null +++ b/src/frontend/src/components/tables/part/RelatedPartTable.tsx @@ -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 ( + { + navigate(`/part/${part.pk}/`); + }} + > + + {part.name} + + ); + } + }, + { + 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( + + + + + + ); + + 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: ( + {t`Are you sure you want to remove this relationship?`} + ), + onFormSuccess: refreshTable + }); + } + } + ]; + }, []); + + return ( + + ); +} diff --git a/src/frontend/src/functions/forms.tsx b/src/frontend/src/functions/forms.tsx index 23babecec1..210f06d147 100644 --- a/src/frontend/src/functions/forms.tsx +++ b/src/frontend/src/functions/forms.tsx @@ -175,7 +175,8 @@ export function openDeleteApiForm(props: ApiFormProps) { ...props, method: 'DELETE', submitText: t`Delete`, - submitColor: 'red' + submitColor: 'red', + fields: {} }; openModalApiForm(deleteProps); diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index 404c0d23e8..de9ac74887 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -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: , - content: part related parts go here + content: partRelatedTab() }, { name: 'attachments', @@ -171,6 +169,9 @@ export default function PartDetail() { ); } + function partRelatedTab(): React.ReactNode { + return ; + } function partNotesTab(): React.ReactNode { // TODO: Set edit permission based on user permissions return (