[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:
Oliver 2023-09-12 16:34:39 +10:00 committed by GitHub
parent 816b60850d
commit 666fe90c5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 145 additions and 14 deletions

View File

@ -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

View File

@ -8,7 +8,7 @@ import { api } from '../../App';
export function Thumbnail({
src,
alt = t`Thumbnail`,
size = 24
size = 20
}: {
src: string;
alt?: string;

View File

@ -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}
/>

View File

@ -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>

View 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}
/>
);
}

View File

@ -175,7 +175,8 @@ export function openDeleteApiForm(props: ApiFormProps) {
...props,
method: 'DELETE',
submitText: t`Delete`,
submitColor: 'red'
submitColor: 'red',
fields: {}
};
openModalApiForm(deleteProps);

View File

@ -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 (