mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +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:
parent
816b60850d
commit
666fe90c5c
@ -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 (
|
||||||
|
Loading…
Reference in New Issue
Block a user