[React] SupplierPart table (#5833)

* Fix TableHoverCard component

* Improving handling of very wide table cells

* Update panels for PartDetail

* Refactor <Thumbnail> component

* Add SupplierPart table

* Refactor forms

- Do not need to specify custom form name any more

* More fixes for modal forms

* Refactor forms field code

* Add generic row action components for edit and delete

* Add placeholder comments

* Add ability to edit supplier part from table

* Create supplier part

* Add missing import

* Revert scroll behaviour for wide cells

- Does not play nice on chrome

* Add placeholder panel for part manufacturers

* Fix inline renderer for manufacturerpart

* Cleanup unused imports

* Add icons to supplier part fields

* Increase size of form titles

* Another fix
This commit is contained in:
Oliver 2023-11-02 12:10:03 +11:00 committed by GitHub
parent a11418398f
commit f71322ecd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 608 additions and 258 deletions

View File

@ -372,6 +372,7 @@ class SupplierPartSerializer(InvenTreeTagModelSerializer):
# Annotated field showing total in-stock quantity
in_stock = serializers.FloatField(read_only=True)
available = serializers.FloatField(required=False)
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)

View File

@ -16,7 +16,6 @@ import { ApiFormField, ApiFormFieldSet } from './fields/ApiFormField';
/**
* Properties for the ApiForm component
* @param name : The name (identifier) for this form
* @param url : The API endpoint to fetch the form data from
* @param pk : Optional primary-key value when editing an existing object
* @param title : The title to display in the form header
@ -35,7 +34,6 @@ import { ApiFormField, ApiFormFieldSet } from './fields/ApiFormField';
* @param onFormError : A callback function to call when the form is submitted with errors.
*/
export interface ApiFormProps {
name: string;
url: ApiPaths;
pk?: number | string | undefined;
title: string;
@ -104,7 +102,7 @@ export function ApiForm({
// Query manager for retrieiving initial data from the server
const initialDataQuery = useQuery({
enabled: false,
queryKey: ['form-initial-data', props.name, props.url, props.pk],
queryKey: ['form-initial-data', modalId, props.method, props.url, props.pk],
queryFn: async () => {
return api
.get(url)
@ -150,7 +148,13 @@ export function ApiForm({
// Fetch initial data if the fetchInitialData property is set
if (props.fetchInitialData) {
queryClient.removeQueries({
queryKey: ['form-initial-data', props.name, props.url, props.pk]
queryKey: [
'form-initial-data',
modalId,
props.method,
props.url,
props.pk
]
});
initialDataQuery.refetch();
}
@ -159,7 +163,7 @@ export function ApiForm({
// Query manager for submitting data
const submitQuery = useQuery({
enabled: false,
queryKey: ['form-submit', props.name, props.url, props.pk],
queryKey: ['form-submit', modalId, props.method, props.url, props.pk],
queryFn: async () => {
let method = props.method?.toLowerCase() ?? 'get';

View File

@ -2,35 +2,43 @@ import { t } from '@lingui/macro';
import { Anchor } from '@mantine/core';
import { Group } from '@mantine/core';
import { Text } from '@mantine/core';
import { useMemo } from 'react';
import { ReactNode, useMemo } from 'react';
import { ApiImage } from './ApiImage';
/*
* Render an image, loaded via the API
*/
export function Thumbnail({
src,
alt = t`Thumbnail`,
size = 20
size = 20,
text
}: {
src?: string | undefined;
alt?: string;
size?: number;
text?: ReactNode;
}) {
// TODO: Use HoverCard to display a larger version of the image
const backup_image = '/static/img/blank_image.png';
return (
<ApiImage
src={src || '/static/img/blank_image.png'}
alt={alt}
width={size}
fit="contain"
radius="xs"
withPlaceholder
imageProps={{
style: {
maxHeight: size
}
}}
/>
<Group align="left" spacing="xs" noWrap={true}>
<ApiImage
src={src || backup_image}
alt={alt}
width={size}
fit="contain"
radius="xs"
withPlaceholder
imageProps={{
style: {
maxHeight: size
}
}}
/>
{text}
</Group>
);
}
@ -39,7 +47,7 @@ export function ThumbnailHoverCard({
text,
link = '',
alt = t`Thumbnail`,
size = 24
size = 20
}: {
src: string;
text: string;
@ -56,12 +64,13 @@ export function ThumbnailHoverCard({
);
}, [src, text, alt, size]);
if (link)
if (link) {
return (
<Anchor href={link} style={{ textDecoration: 'none' }}>
{card}
</Anchor>
);
}
return <div>{card}</div>;
}

View File

@ -70,15 +70,20 @@ export function RenderSupplierPart({ instance }: { instance: any }): ReactNode {
/**
* Inline rendering of a single ManufacturerPart instance
*/
export function ManufacturerPart({ instance }: { instance: any }): ReactNode {
let supplier = instance.supplier_detail ?? {};
export function RenderManufacturerPart({
instance
}: {
instance: any;
}): ReactNode {
let part = instance.part_detail ?? {};
let manufacturer = instance.manufacturer_detail ?? {};
let text = instance.SKU;
if (supplier.name) {
text = `${supplier.name} | ${text}`;
}
return <RenderInlineModel primary={text} secondary={part.full_name} />;
return (
<RenderInlineModel
primary={manufacturer.name}
secondary={instance.MPN}
suffix={part.full_name}
image={manufacturer?.thumnbnail ?? manufacturer.image}
/>
);
}

View File

@ -9,6 +9,7 @@ import {
RenderAddress,
RenderCompany,
RenderContact,
RenderManufacturerPart,
RenderSupplierPart
} from './Company';
import { ModelType } from './ModelType';
@ -41,6 +42,7 @@ const RendererLookup: EnumDictionary<
[ModelType.build]: RenderBuildOrder,
[ModelType.company]: RenderCompany,
[ModelType.contact]: RenderContact,
[ModelType.manufacturerpart]: RenderManufacturerPart,
[ModelType.owner]: RenderOwner,
[ModelType.part]: RenderPart,
[ModelType.partcategory]: RenderPartCategory,
@ -54,8 +56,7 @@ const RendererLookup: EnumDictionary<
[ModelType.stockitem]: RenderStockItem,
[ModelType.stockhistory]: RenderStockItem,
[ModelType.supplierpart]: RenderSupplierPart,
[ModelType.user]: RenderUser,
[ModelType.manufacturerpart]: RenderPart
[ModelType.user]: RenderUser
};
// import { ApiFormFieldType } from "../forms/fields/ApiFormField";

View File

@ -51,7 +51,6 @@ function SettingValue({
}
openModalApiForm({
name: 'setting-edit',
url: settingsState.endpoint,
pk: setting.key,
method: 'PATCH',

View File

@ -524,7 +524,11 @@ export function InvenTreeTable({
onRowClick={tableProps.onRowClick}
defaultColumnProps={{
noWrap: true,
textAlignment: 'left'
textAlignment: 'left',
cellsStyle: {
// TODO: Need a better way of handling "wide" cells,
overflow: 'hidden'
}
}}
/>
</Stack>

View File

@ -2,7 +2,7 @@ import { t } from '@lingui/macro';
import { ActionIcon, Tooltip } from '@mantine/core';
import { Menu, Text } from '@mantine/core';
import { IconDots } from '@tabler/icons-react';
import { ReactNode, useState } from 'react';
import { ReactNode, useMemo, useState } from 'react';
import { notYetImplemented } from '../../functions/notifications';
@ -12,9 +12,41 @@ export type RowAction = {
color?: string;
onClick?: () => void;
tooltip?: string;
icon?: ReactNode;
hidden?: boolean;
};
// Component for ediitng a row in a table
export function RowEditAction({
onClick,
hidden
}: {
onClick?: () => void;
hidden?: boolean;
}): RowAction {
return {
title: t`Edit`,
color: 'green',
onClick: onClick,
hidden: hidden
};
}
// Component for deleting a row in a table
export function RowDeleteAction({
onClick,
hidden
}: {
onClick?: () => void;
hidden?: boolean;
}): RowAction {
return {
title: t`Delete`,
color: 'red',
onClick: onClick,
hidden: hidden
};
}
/**
* Component for displaying actions for a row in a table.
* Displays a simple dropdown menu with a list of actions.
@ -39,8 +71,12 @@ export function RowActions({
const [opened, setOpened] = useState(false);
const visibleActions = useMemo(() => {
return actions.filter((action) => !action.hidden);
}, [actions]);
return (
actions.length > 0 && (
visibleActions.length > 0 && (
<Menu
withinPortal={true}
disabled={disabled}
@ -61,7 +97,7 @@ export function RowActions({
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>{title || t`Actions`}</Menu.Label>
{actions.map((action, idx) => (
{visibleActions.map((action, idx) => (
<Menu.Item
key={idx}
onClick={(event) => {
@ -75,7 +111,6 @@ export function RowActions({
notYetImplemented();
}
}}
icon={action.icon}
title={action.tooltip || action.title}
>
<Text size="xs" color={action.color}>

View File

@ -23,7 +23,7 @@ export function TableHoverCard({
return (
<HoverCard>
<HoverCard.Target>
<Group spacing="xs" position="apart">
<Group spacing="xs" position="apart" noWrap={true}>
{value}
<IconInfoCircle size="16" color="blue" />
</Group>

View File

@ -11,7 +11,7 @@ import { YesNoButton } from '../../items/YesNoButton';
import { TableColumn } from '../Column';
import { TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
import { RowAction } from '../RowActions';
import { RowAction, RowDeleteAction, RowEditAction } from '../RowActions';
import { TableHoverCard } from '../TableHoverCard';
export function BomTable({
@ -204,14 +204,11 @@ export function BomTable({
});
}
actions.push({
title: t`Edit`
});
// TODO: Action on edit
actions.push(RowEditAction({}));
actions.push({
title: t`Delete`,
color: 'red'
});
// TODO: Action on delete
actions.push(RowDeleteAction({}));
return actions;
},

View File

@ -11,13 +11,13 @@ import {
addAttachment,
deleteAttachment,
editAttachment
} from '../../../functions/forms/AttachmentForms';
} from '../../../forms/AttachmentForms';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { AttachmentLink } from '../../items/AttachmentLink';
import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable';
import { RowAction } from '../RowActions';
import { RowAction, RowDeleteAction, RowEditAction } from '../RowActions';
/**
* Define set of columns to display for the attachment table
@ -113,32 +113,33 @@ export function AttachmentTable({
let actions: RowAction[] = [];
if (allowEdit) {
actions.push({
title: t`Edit`,
onClick: () => {
editAttachment({
endpoint: endpoint,
model: model,
pk: record.pk,
attachmentType: record.attachment ? 'file' : 'link',
callback: refreshTable
});
}
});
actions.push(
RowEditAction({
onClick: () => {
editAttachment({
endpoint: endpoint,
model: model,
pk: record.pk,
attachmentType: record.attachment ? 'file' : 'link',
callback: refreshTable
});
}
})
);
}
if (allowDelete) {
actions.push({
title: t`Delete`,
color: 'red',
onClick: () => {
deleteAttachment({
endpoint: endpoint,
pk: record.pk,
callback: refreshTable
});
}
});
actions.push(
RowDeleteAction({
onClick: () => {
deleteAttachment({
endpoint: endpoint,
pk: record.pk,
callback: refreshTable
});
}
})
);
}
return actions;

View File

@ -14,6 +14,7 @@ import { Thumbnail } from '../../images/Thumbnail';
import { YesNoButton } from '../../items/YesNoButton';
import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable';
import { RowDeleteAction, RowEditAction } from '../RowActions';
/**
* Construct a table listing parameters for a given part
@ -103,44 +104,43 @@ export function PartParameterTable({ partId }: { partId: any }) {
let actions = [];
actions.push({
title: t`Edit`,
onClick: () => {
openEditApiForm({
name: 'edit-part-parameter',
url: ApiPaths.part_parameter_list,
pk: record.pk,
title: t`Edit Part Parameter`,
fields: {
part: {
hidden: true
actions.push(
RowEditAction({
onClick: () => {
openEditApiForm({
url: ApiPaths.part_parameter_list,
pk: record.pk,
title: t`Edit Part Parameter`,
fields: {
part: {
hidden: true
},
template: {},
data: {}
},
template: {},
data: {}
},
successMessage: t`Part parameter updated`,
onFormSuccess: refreshTable
});
}
});
successMessage: t`Part parameter updated`,
onFormSuccess: refreshTable
});
}
})
);
actions.push({
title: t`Delete`,
color: 'red',
onClick: () => {
openDeleteApiForm({
name: 'delete-part-parameter',
url: ApiPaths.part_parameter_list,
pk: record.pk,
title: t`Delete Part Parameter`,
successMessage: t`Part parameter deleted`,
onFormSuccess: refreshTable,
preFormContent: (
<Text>{t`Are you sure you want to remove this parameter?`}</Text>
)
});
}
});
actions.push(
RowDeleteAction({
onClick: () => {
openDeleteApiForm({
url: ApiPaths.part_parameter_list,
pk: record.pk,
title: t`Delete Part Parameter`,
successMessage: t`Part parameter deleted`,
onFormSuccess: refreshTable,
preFormContent: (
<Text>{t`Are you sure you want to remove this parameter?`}</Text>
)
});
}
})
);
return actions;
},
@ -153,7 +153,6 @@ export function PartParameterTable({ partId }: { partId: any }) {
}
openCreateApiForm({
name: 'add-part-parameter',
url: ApiPaths.part_parameter_list,
title: t`Add Part Parameter`,
fields: {

View File

@ -23,16 +23,12 @@ function partTableColumns(): TableColumn[] {
noWrap: true,
title: t`Part`,
render: function (record: any) {
// TODO - Link to the part detail page
return (
<Group spacing="xs" align="left" noWrap={true}>
<Thumbnail
src={record.thumbnail || record.image}
alt={record.name}
size={24}
/>
<Text>{record.full_name}</Text>
</Group>
<Thumbnail
src={record.thumbnail || record.image}
alt={record.name}
text={record.full_name}
/>
);
}
},

View File

@ -10,6 +10,7 @@ import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { Thumbnail } from '../../images/Thumbnail';
import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable';
import { RowDeleteAction } from '../RowActions';
/**
* Construct a table listing related parts for a given part
@ -33,7 +34,6 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
{
accessor: 'part',
title: t`Part`,
noWrap: true,
render: (record: any) => {
let part = getPart(record);
return (
@ -63,7 +63,6 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
const addRelatedPart = useCallback(() => {
openCreateApiForm({
name: 'add-related-part',
title: t`Add Related Part`,
url: ApiPaths.related_part_list,
fields: {
@ -99,12 +98,9 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
// TODO: Hide if user does not have permission to edit parts
const rowActions = useCallback((record: any) => {
return [
{
title: t`Delete`,
color: 'red',
RowDeleteAction({
onClick: () => {
openDeleteApiForm({
name: 'delete-related-part',
url: ApiPaths.related_part_list,
pk: record.pk,
title: t`Delete Related Part`,
@ -115,7 +111,7 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
onFormSuccess: refreshTable
});
}
}
})
];
}, []);

View File

@ -1,5 +1,4 @@
import { t } from '@lingui/macro';
import { Group, Text } from '@mantine/core';
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
@ -45,10 +44,11 @@ export function PurchaseOrderTable({ params }: { params?: any }) {
let supplier = record.supplier_detail ?? {};
return (
<Group spacing="xs" noWrap={true}>
<Thumbnail src={supplier?.image} alt={supplier.name} />
<Text>{supplier?.name}</Text>
</Group>
<Thumbnail
src={supplier?.image}
alt={supplier.name}
text={supplier.name}
/>
);
}
},

View File

@ -0,0 +1,258 @@
import { t } from '@lingui/macro';
import { ActionIcon, Stack, Text, Tooltip } from '@mantine/core';
import { IconCirclePlus } from '@tabler/icons-react';
import { ReactNode, useCallback, useMemo } from 'react';
import { supplierPartFields } from '../../../forms/CompanyForms';
import {
openCreateApiForm,
openDeleteApiForm,
openEditApiForm
} from '../../../functions/forms';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState';
import { Thumbnail } from '../../images/Thumbnail';
import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable';
import { RowDeleteAction, RowEditAction } from '../RowActions';
import { TableHoverCard } from '../TableHoverCard';
/*
* Construct a table listing supplier parts
*/
export function SupplierPartTable({ params }: { params: any }): ReactNode {
const { tableKey, refreshTable } = useTableRefresh('supplierparts');
const user = useUserState();
// Construct table columns for this table
const tableColumns: TableColumn[] = useMemo(() => {
return [
{
accessor: 'part',
title: t`Part`,
switchable: 'part' in params,
sortable: true,
render: (record: any) => {
let part = record?.part_detail ?? {};
return (
<Thumbnail src={part?.thumbnail ?? part.image} text={part.name} />
);
}
},
{
accessor: 'supplier',
title: t`Supplier`,
sortable: true,
render: (record: any) => {
let supplier = record?.supplier_detail ?? {};
return (
<Thumbnail
src={supplier?.thumbnail ?? supplier.image}
text={supplier.name}
/>
);
}
},
{
accessor: 'SKU',
title: t`Supplier Part`,
sortable: true
},
{
accessor: 'description',
title: t`Description`,
sortable: false,
switchable: true
},
{
accessor: 'manufacturer',
switchable: true,
sortable: true,
title: t`Manufacturer`,
render: (record: any) => {
let manufacturer = record?.manufacturer_detail ?? {};
return (
<Thumbnail
src={manufacturer?.thumbnail ?? manufacturer.image}
text={manufacturer.name}
/>
);
}
},
{
accessor: 'MPN',
switchable: true,
sortable: true,
title: t`MPN`,
render: (record: any) => record?.manufacturer_part_detail?.MPN
},
{
accessor: 'in_stock',
title: t`In Stock`,
sortable: true,
switchable: true
},
{
accessor: 'packaging',
title: t`Packaging`,
sortable: true,
switchable: true
},
{
accessor: 'pack_quantity',
title: t`Pack Quantity`,
sortable: true,
switchable: true,
render: (record: any) => {
let part = record?.part_detail ?? {};
let extra = [];
if (part.units) {
extra.push(
<Text>
{t`Base units`} : {part.units}
</Text>
);
}
return (
<TableHoverCard
value={record.pack_quantity}
extra={extra.length > 0 && <Stack spacing="xs">{extra}</Stack>}
title={t`Pack Quantity`}
/>
);
}
},
{
accessor: 'link',
title: t`Link`,
sortable: false,
switchable: true
// TODO: custom link renderer?
},
{
accessor: 'note',
title: t`Notes`,
sortable: false,
switchable: true
},
{
accessor: 'available',
title: t`Availability`,
sortable: true,
switchable: true,
render: (record: any) => {
let extra = [];
if (record.availablility_updated) {
extra.push(
<Text>
{t`Updated`} : {record.availablility_updated}
</Text>
);
}
return (
<TableHoverCard
value={record.available}
extra={extra.length > 0 && <Stack spacing="xs">{extra}</Stack>}
/>
);
}
}
];
}, [params]);
const addSupplierPart = useCallback(() => {
let fields = supplierPartFields();
fields.part.value = params?.part;
fields.supplier.value = params?.supplier;
openCreateApiForm({
url: ApiPaths.supplier_part_list,
title: t`Add Supplier Part`,
fields: fields,
onFormSuccess: refreshTable,
successMessage: t`Supplier part created`
});
}, [params]);
// Table actions
const tableActions = useMemo(() => {
// TODO: Hide actions based on user permissions
return [
// TODO: Refactor this component out to something reusable
<Tooltip label={t`Add supplier part`}>
<ActionIcon radius="sm" onClick={addSupplierPart}>
<IconCirclePlus color="green" />
</ActionIcon>
</Tooltip>
];
}, [user]);
// Row action callback
const rowActions = useCallback(
(record: any) => {
// TODO: Adjust actions based on user permissions
return [
RowEditAction({
onClick: () => {
record.pk &&
openEditApiForm({
url: ApiPaths.supplier_part_list,
pk: record.pk,
title: t`Edit Supplier Part`,
fields: supplierPartFields(),
onFormSuccess: refreshTable,
successMessage: t`Supplier part updated`
});
}
}),
RowDeleteAction({
onClick: () => {
record.pk &&
openDeleteApiForm({
url: ApiPaths.supplier_part_list,
pk: record.pk,
title: t`Delete Supplier Part`,
successMessage: t`Supplier part deleted`,
onFormSuccess: refreshTable,
preFormContent: (
<Text>{t`Are you sure you want to remove this supplier part?`}</Text>
)
});
}
})
];
},
[user]
);
return (
<InvenTreeTable
url={apiUrl(ApiPaths.supplier_part_list)}
tableKey={tableKey}
columns={tableColumns}
props={{
params: {
...params,
part_detail: true,
supplier_detail: true,
manufacturer_detail: true
},
rowActions: rowActions,
customActionGroups: tableActions
}}
/>
);
}

View File

@ -1,5 +1,4 @@
import { t } from '@lingui/macro';
import { Group, Text } from '@mantine/core';
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
@ -41,10 +40,11 @@ export function ReturnOrderTable({ params }: { params?: any }) {
let customer = record.customer_detail ?? {};
return (
<Group spacing="xs" noWrap={true}>
<Thumbnail src={customer?.image} alt={customer.name} />
<Text>{customer?.name}</Text>
</Group>
<Thumbnail
src={customer?.image}
alt={customer.name}
text={customer.name}
/>
);
}
},

View File

@ -1,5 +1,4 @@
import { t } from '@lingui/macro';
import { Group, Text } from '@mantine/core';
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
@ -42,10 +41,11 @@ export function SalesOrderTable({ params }: { params?: any }) {
let customer = record.customer_detail ?? {};
return (
<Group spacing="xs" noWrap={true}>
<Thumbnail src={customer?.image} alt={customer.name} />
<Text>{customer?.name}</Text>
</Group>
<Thumbnail
src={customer?.image}
alt={customer.name}
text={customer.name}
/>
);
}
},

View File

@ -12,7 +12,7 @@ import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable';
import { RowAction } from '../RowActions';
import { RowAction, RowDeleteAction, RowEditAction } from '../RowActions';
/**
* Table for displaying list of custom physical units
@ -45,11 +45,9 @@ export function CustomUnitsTable() {
const rowActions = useCallback((record: any): RowAction[] => {
return [
{
title: t`Edit`,
RowEditAction({
onClick: () => {
openEditApiForm({
name: 'edit-custom-unit',
url: ApiPaths.custom_unit_list,
pk: record.pk,
title: t`Edit custom unit`,
@ -62,12 +60,10 @@ export function CustomUnitsTable() {
successMessage: t`Custom unit updated`
});
}
},
{
title: t`Delete`,
}),
RowDeleteAction({
onClick: () => {
openDeleteApiForm({
name: 'delete-custom-unit',
url: ApiPaths.custom_unit_list,
pk: record.pk,
title: t`Delete custom unit`,
@ -78,13 +74,12 @@ export function CustomUnitsTable() {
)
});
}
}
})
];
}, []);
const addCustomUnit = useCallback(() => {
openCreateApiForm({
name: 'add-custom-unit',
url: ApiPaths.custom_unit_list,
title: t`Add custom unit`,
fields: {

View File

@ -12,7 +12,7 @@ import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable';
import { RowAction } from '../RowActions';
import { RowAction, RowDeleteAction, RowEditAction } from '../RowActions';
/**
* Table for displaying list of project codes
@ -37,11 +37,9 @@ export function ProjectCodeTable() {
const rowActions = useCallback((record: any): RowAction[] => {
return [
{
title: t`Edit`,
RowEditAction({
onClick: () => {
openEditApiForm({
name: 'edit-project-code',
url: ApiPaths.project_code_list,
pk: record.pk,
title: t`Edit project code`,
@ -53,13 +51,10 @@ export function ProjectCodeTable() {
successMessage: t`Project code updated`
});
}
},
{
title: t`Delete`,
color: 'red',
}),
RowDeleteAction({
onClick: () => {
openDeleteApiForm({
name: 'delete-project-code',
url: ApiPaths.project_code_list,
pk: record.pk,
title: t`Delete project code`,
@ -70,13 +65,12 @@ export function ProjectCodeTable() {
)
});
}
}
})
];
}, []);
const addProjectCode = useCallback(() => {
openCreateApiForm({
name: 'add-project-code',
url: ApiPaths.project_code_list,
title: t`Add project code`,
fields: {

View File

@ -3,7 +3,6 @@ import { Group, Stack, Text } from '@mantine/core';
import { ReactNode, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { notYetImplemented } from '../../../functions/notifications';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { Thumbnail } from '../../images/Thumbnail';
@ -219,13 +218,7 @@ export function StockItemTable({ params = {} }: { params?: any }) {
function stockItemRowActions(record: any): RowAction[] {
let actions: RowAction[] = [];
actions.push({
title: t`Edit`,
onClick: () => {
notYetImplemented();
}
});
// TODO: Custom row actions for stock table
return actions;
}

View File

@ -1,13 +1,13 @@
import { t } from '@lingui/macro';
import { Text } from '@mantine/core';
import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField';
import { ApiPaths } from '../../states/ApiState';
import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField';
import {
openCreateApiForm,
openDeleteApiForm,
openEditApiForm
} from '../forms';
} from '../functions/forms';
import { ApiPaths } from '../states/ApiState';
export function attachmentFields(editing: boolean): ApiFormFieldSet {
let fields: ApiFormFieldSet = {
@ -59,7 +59,6 @@ export function addAttachment({
let message = attachmentType === 'file' ? t`File added` : t`Link added`;
openCreateApiForm({
name: 'attachment-add',
title: title,
url: endpoint,
successMessage: message,
@ -102,7 +101,6 @@ export function editAttachment({
let message = attachmentType === 'file' ? t`File updated` : t`Link updated`;
openEditApiForm({
name: 'attachment-edit',
title: title,
url: endpoint,
pk: pk,
@ -124,7 +122,6 @@ export function deleteAttachment({
openDeleteApiForm({
url: endpoint,
pk: pk,
name: 'attachment-edit',
title: t`Delete Attachment`,
successMessage: t`Attachment deleted`,
onFormSuccess: callback,

View File

@ -0,0 +1,106 @@
import { t } from '@lingui/macro';
import {
IconAt,
IconCurrencyDollar,
IconGlobe,
IconHash,
IconLink,
IconNote,
IconPackage,
IconPhone
} from '@tabler/icons-react';
import {
ApiFormData,
ApiFormFieldSet
} from '../components/forms/fields/ApiFormField';
import { openEditApiForm } from '../functions/forms';
import { ApiPaths } from '../states/ApiState';
/**
* Field set for SupplierPart instance
*/
export function supplierPartFields(): ApiFormFieldSet {
return {
part: {
filters: {
purchaseable: true
}
},
manufacturer_part: {
filters: {
part_detail: true,
manufacturer_detail: true
},
adjustFilters: (filters: any, form: ApiFormData) => {
let part = form.values.part;
if (part) {
filters.part = part;
}
return filters;
}
},
supplier: {},
SKU: {
icon: <IconHash />
},
description: {},
link: {
icon: <IconLink />
},
note: {
icon: <IconNote />
},
pack_quantity: {},
packaging: {
icon: <IconPackage />
}
};
}
/**
* Field set for editing a company instance
*/
export function companyFields(): ApiFormFieldSet {
return {
name: {},
description: {},
website: {
icon: <IconGlobe />
},
currency: {
icon: <IconCurrencyDollar />
},
phone: {
icon: <IconPhone />
},
email: {
icon: <IconAt />
},
is_supplier: {},
is_manufacturer: {},
is_customer: {}
};
}
/**
* Edit a company instance
*/
export function editCompany({
pk,
callback
}: {
pk: number;
callback?: () => void;
}) {
openEditApiForm({
title: t`Edit Company`,
url: ApiPaths.company_list,
pk: pk,
fields: companyFields(),
successMessage: t`Company updated`,
onFormSuccess: callback
});
}

View File

@ -1,8 +1,8 @@
import { t } from '@lingui/macro';
import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField';
import { ApiPaths } from '../../states/ApiState';
import { openCreateApiForm, openEditApiForm } from '../forms';
import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField';
import { openCreateApiForm, openEditApiForm } from '../functions/forms';
import { ApiPaths } from '../states/ApiState';
/**
* Construct a set of fields for creating / editing a Part instance
@ -70,7 +70,6 @@ export function partFields({
*/
export function createPart() {
openCreateApiForm({
name: 'part-create',
title: t`Create Part`,
url: ApiPaths.part_list,
successMessage: t`Part created`,
@ -90,7 +89,6 @@ export function editPart({
callback?: () => void;
}) {
openEditApiForm({
name: 'part-edit',
title: t`Edit Part`,
url: ApiPaths.part_list,
pk: part_id,

View File

@ -4,9 +4,9 @@ import {
ApiFormChangeCallback,
ApiFormData,
ApiFormFieldSet
} from '../../components/forms/fields/ApiFormField';
import { ApiPaths } from '../../states/ApiState';
import { openCreateApiForm, openEditApiForm } from '../forms';
} from '../components/forms/fields/ApiFormField';
import { openCreateApiForm, openEditApiForm } from '../functions/forms';
import { ApiPaths } from '../states/ApiState';
/**
* Construct a set of fields for creating / editing a StockItem instance
@ -30,7 +30,6 @@ export function stockFields({
},
supplier_part: {
// TODO: icon
// TODO: implement adjustFilters
filters: {
part_detail: true,
supplier_detail: true
@ -107,7 +106,6 @@ export function stockFields({
*/
export function createStockItem() {
openCreateApiForm({
name: 'stockitem-create',
url: ApiPaths.stock_item_list,
fields: stockFields({ create: true }),
title: t`Create Stock Item`
@ -126,7 +124,6 @@ export function editStockItem({
callback?: () => void;
}) {
openEditApiForm({
name: 'stockitem-edit',
url: ApiPaths.stock_item_list,
pk: item_id,
fields: stockFields({ create: false }),

View File

@ -115,10 +115,12 @@ export function openModalApiForm(props: ApiFormProps) {
}
// Generate a random modal ID for controller
let modalId: string = `modal-${props.title}-` + generateUniqueId();
let modalId: string =
`modal-${props.title}-${props.url}-${props.method}` +
generateUniqueId();
modals.open({
title: <StylishText>{props.title}</StylishText>,
title: <StylishText size="xl">{props.title}</StylishText>,
modalId: modalId,
size: 'xl',
onClose: () => {

View File

@ -1,57 +0,0 @@
import { t } from '@lingui/macro';
import {
IconAt,
IconCurrencyDollar,
IconGlobe,
IconPhone
} from '@tabler/icons-react';
import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField';
import { ApiPaths } from '../../states/ApiState';
import { openEditApiForm } from '../forms';
/**
* Field set for editing a company instance
*/
export function companyFields(): ApiFormFieldSet {
return {
name: {},
description: {},
website: {
icon: <IconGlobe />
},
currency: {
icon: <IconCurrencyDollar />
},
phone: {
icon: <IconPhone />
},
email: {
icon: <IconAt />
},
is_supplier: {},
is_manufacturer: {},
is_customer: {}
};
}
/**
* Edit a company instance
*/
export function editCompany({
pk,
callback
}: {
pk: number;
callback?: () => void;
}) {
openEditApiForm({
name: 'company-edit',
title: t`Edit Company`,
url: ApiPaths.company_list,
pk: pk,
fields: companyFields(),
successMessage: t`Company updated`,
onFormSuccess: callback
});
}

View File

@ -9,13 +9,13 @@ import { PlaceholderPill } from '../../components/items/Placeholder';
import { StylishText } from '../../components/items/StylishText';
import { ModelType } from '../../components/render/ModelType';
import { StatusRenderer } from '../../components/renderers/StatusRenderer';
import { openCreateApiForm, openEditApiForm } from '../../functions/forms';
import {
createPart,
editPart,
partCategoryFields
} from '../../functions/forms/PartForms';
import { createStockItem } from '../../functions/forms/StockForms';
} from '../../forms/PartForms';
import { createStockItem } from '../../forms/StockForms';
import { openCreateApiForm, openEditApiForm } from '../../functions/forms';
import { ApiPaths } from '../../states/ApiState';
// Generate some example forms using the modal API forms interface
@ -23,7 +23,6 @@ function ApiFormsPlayground() {
let fields = partCategoryFields({});
const editCategoryForm: ApiFormProps = {
name: 'partcategory',
url: ApiPaths.category_list,
pk: 2,
title: 'Edit Category',
@ -31,7 +30,6 @@ function ApiFormsPlayground() {
};
const createAttachmentForm: ApiFormProps = {
name: 'createattachment',
url: ApiPaths.part_attachment_list,
title: 'Create Attachment',
successMessage: 'Attachment uploaded',

View File

@ -31,7 +31,7 @@ import { ReturnOrderTable } from '../../components/tables/sales/ReturnOrderTable
import { SalesOrderTable } from '../../components/tables/sales/SalesOrderTable';
import { StockItemTable } from '../../components/tables/stock/StockItemTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { editCompany } from '../../functions/forms/CompanyForms';
import { editCompany } from '../../forms/CompanyForms';
import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';

View File

@ -1,7 +1,9 @@
import { t } from '@lingui/macro';
import { Group, LoadingOverlay, Stack, Text } from '@mantine/core';
import {
IconBookmarks,
IconBuilding,
IconBuildingFactory2,
IconCalendarStats,
IconClipboardList,
IconCopy,
@ -44,10 +46,11 @@ import { AttachmentTable } from '../../components/tables/general/AttachmentTable
import { PartParameterTable } from '../../components/tables/part/PartParameterTable';
import { PartVariantTable } from '../../components/tables/part/PartVariantTable';
import { RelatedPartTable } from '../../components/tables/part/RelatedPartTable';
import { SupplierPartTable } from '../../components/tables/purchasing/SupplierPartTable';
import { SalesOrderTable } from '../../components/tables/sales/SalesOrderTable';
import { StockItemTable } from '../../components/tables/stock/StockItemTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { editPart } from '../../functions/forms/PartForms';
import { editPart } from '../../forms/PartForms';
import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
@ -108,6 +111,12 @@ export default function PartDetail() {
hidden: !part.is_template,
content: <PartVariantTable partId={String(id)} />
},
{
name: 'allocations',
label: t`Allocations`,
icon: <IconBookmarks />,
hidden: !part.component && !part.salable
},
{
name: 'bom',
label: t`Bill of Materials`,
@ -119,7 +128,7 @@ export default function PartDetail() {
name: 'builds',
label: t`Build Orders`,
icon: <IconTools />,
hidden: !part.assembly && !part.component,
hidden: !part.assembly,
content: (
<BuildOrderTable
params={{
@ -141,11 +150,24 @@ export default function PartDetail() {
label: t`Pricing`,
icon: <IconCurrencyDollar />
},
{
name: 'manufacturers',
label: t`Manufacturers`,
icon: <IconBuildingFactory2 />,
hidden: !part.purchaseable
},
{
name: 'suppliers',
label: t`Suppliers`,
icon: <IconBuilding />,
hidden: !part.purchaseable
hidden: !part.purchaseable,
content: part.pk && (
<SupplierPartTable
params={{
part: part.pk ?? -1
}}
/>
)
},
{
name: 'purchase_orders',

View File

@ -35,7 +35,7 @@ import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { StockLocationTree } from '../../components/nav/StockLocationTree';
import { AttachmentTable } from '../../components/tables/general/AttachmentTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { editStockItem } from '../../functions/forms/StockForms';
import { editStockItem } from '../../forms/StockForms';
import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';