2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-10-24 01:47:39 +00:00

[UI] Part import (#10609)

* Allow import of parts from file

* Extend default values for part import

* Small visual tweaks

* Update documentation

* Update playwright test
This commit is contained in:
Oliver
2025-10-18 18:12:05 +11:00
committed by GitHub
parent 72d127219f
commit 803d22155b
11 changed files with 162 additions and 71 deletions

View File

@@ -188,6 +188,7 @@ export function PrintingActions({
<ActionDropdown
tooltip={t`Printing Actions`}
icon={<IconPrinter />}
position='bottom-start'
disabled={!enabled}
actions={[
{

View File

@@ -48,7 +48,8 @@ export function ActionDropdown({
actions,
disabled = false,
hidden = false,
noindicator = false
noindicator = false,
position
}: {
icon: ReactNode;
tooltip: string;
@@ -57,6 +58,7 @@ export function ActionDropdown({
disabled?: boolean;
hidden?: boolean;
noindicator?: boolean;
position?: FloatingPosition;
}): ReactNode {
const hasActions = useMemo(() => {
return actions.some((action) => !action.hidden);
@@ -71,7 +73,7 @@ export function ActionDropdown({
}, [tooltip]);
return !hidden && hasActions ? (
<Menu position='bottom-end' key={menuName}>
<Menu position={position ?? 'bottom-end'} key={menuName}>
<Indicator disabled={!indicatorProps} {...indicatorProps?.indicator}>
<Menu.Target>
<Tooltip

View File

@@ -21,7 +21,7 @@ import {
Tooltip
} from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import { IconArrowDown, IconPlus } from '@tabler/icons-react';
import { IconArrowDown, IconPlus, IconSearch } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import {
type FormEventHandler,
@@ -197,7 +197,7 @@ const SearchStep = ({
aria-label='textbox-search-for-part'
flex={1}
placeholder='Search for a part'
label={t`Search...`}
label={t`Search`}
value={searchValue}
onChange={(event) => setSearchValue(event.currentTarget.value)}
/>
@@ -224,7 +224,12 @@ const SearchStep = ({
: t`Select supplier`
}
/>
<Button disabled={!searchValue || !supplier} type='submit'>
<Button
color='blue'
disabled={!searchValue || !supplier}
type='submit'
leftSection={<IconSearch />}
>
<Trans>Search</Trans>
</Button>
</Group>
@@ -794,7 +799,7 @@ export default function ImportPartWizard({
// Create the wizard manager
const wizard = useWizard({
title: t`Import Part`,
title: t`Import Supplier Part`,
steps: [
t`Search Supplier Part`,
// if partId is provided, a inventree part already exists, just import the mp/sp

View File

@@ -1,9 +1,3 @@
import { t } from '@lingui/core/macro';
import { Group, Text } from '@mantine/core';
import { type ReactNode, useCallback, useMemo, useState } from 'react';
import { ActionButton } from '@lib/components/ActionButton';
import { AddItemButton } from '@lib/components/AddItemButton';
import {
type RowAction,
RowDuplicateAction,
@@ -17,11 +11,21 @@ import type { TableFilter } from '@lib/types/Filters';
import type { ApiFormFieldSet } from '@lib/types/Forms';
import type { TableColumn } from '@lib/types/Tables';
import type { InvenTreeTableProps } from '@lib/types/Tables';
import { IconPackageImport, IconShoppingCart } from '@tabler/icons-react';
import { t } from '@lingui/core/macro';
import { Group, Text } from '@mantine/core';
import {
IconFileUpload,
IconPackageImport,
IconPlus,
IconShoppingCart
} from '@tabler/icons-react';
import { type ReactNode, useCallback, useMemo, useState } from 'react';
import ImporterDrawer from '../../components/importer/ImporterDrawer';
import { ActionDropdown } from '../../components/items/ActionDropdown';
import ImportPartWizard from '../../components/wizards/ImportPartWizard';
import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
import { formatDecimal, formatPriceRange } from '../../defaults/formatters';
import { dataImporterSessionFields } from '../../forms/ImporterForms';
import { usePartFields } from '../../forms/PartForms';
import { InvenTreeIcon } from '../../functions/icons';
import {
@@ -331,10 +335,12 @@ function partTableFilters(): TableFilter[] {
* @returns
*/
export function PartListTable({
enableImport = true,
props,
defaultPartData
}: Readonly<{
props: InvenTreeTableProps;
enableImport?: boolean;
props?: InvenTreeTableProps;
defaultPartData?: any;
}>) {
const tableColumns = useMemo(() => partTableColumns(), []);
@@ -344,9 +350,39 @@ export function PartListTable({
const user = useUserState();
const globalSettings = useGlobalSettingsState();
const [importOpened, setImportOpened] = useState<boolean>(false);
const [selectedSession, setSelectedSession] = useState<number | undefined>(
undefined
);
const importSessionFields = useMemo(() => {
const fields = dataImporterSessionFields({
modelType: ModelType.part
});
// Override default field values with provided fields
fields.field_defaults.value = {
...props?.params,
...defaultPartData
};
return fields;
}, [defaultPartData, props?.params]);
const importParts = useCreateApiFormModal({
url: ApiEndpoints.import_session_list,
title: t`Import Parts`,
fields: importSessionFields,
onFormSuccess: (response: any) => {
setSelectedSession(response.pk);
setImportOpened(true);
}
});
const initialPartData = useMemo(() => {
return defaultPartData ?? props.params ?? {};
}, [defaultPartData, props.params]);
return defaultPartData ?? props?.params ?? {};
}, [defaultPartData, props?.params]);
const newPart = useCreateApiFormModal({
url: ApiEndpoints.part_list,
@@ -463,6 +499,7 @@ export function PartListTable({
tooltip={t`Part Actions`}
icon={<InvenTreeIcon icon='part' />}
disabled={!table.hasSelectedRecords}
position='bottom-start'
actions={[
{
name: t`Set Category`,
@@ -485,24 +522,37 @@ export function PartListTable({
}
]}
/>,
<AddItemButton
key='add-part'
<ActionDropdown
key='add-parts-actions'
tooltip={t`Add Parts`}
position='bottom-start'
icon={<IconPlus />}
hidden={!user.hasAddRole(UserRoles.part)}
tooltip={t`Add Part`}
onClick={() => newPart.open()}
/>,
<ActionButton
key='import-part'
hidden={
supplierPlugins.length === 0 || !user.hasAddRole(UserRoles.part)
}
tooltip={t`Import Part`}
color='green'
icon={<IconPackageImport />}
onClick={() => importPartWizard.openWizard()}
actions={[
{
name: t`Create Part`,
icon: <IconPlus />,
tooltip: t`Create a new part`,
onClick: () => newPart.open()
},
{
name: t`Import from File`,
icon: <IconFileUpload />,
tooltip: t`Import parts from a file`,
onClick: () => importParts.open(),
hidden: !enableImport
},
{
name: t`Import from Supplier`,
icon: <IconPackageImport />,
tooltip: t`Import parts from a supplier plugin`,
hidden: !enableImport || supplierPlugins.length === 0,
onClick: () => importPartWizard.openWizard()
}
]}
/>
];
}, [user, table.hasSelectedRecords, supplierPlugins]);
}, [user, enableImport, table.hasSelectedRecords, supplierPlugins]);
return (
<>
@@ -510,6 +560,7 @@ export function PartListTable({
{duplicatePart.modal}
{editPart.modal}
{setCategory.modal}
{importParts.modal}
{orderPartsWizard.wizard}
{importPartWizard.wizard}
<InvenTreeTable
@@ -527,12 +578,21 @@ export function PartListTable({
enableReports: true,
enableLabels: true,
params: {
...props.params,
...props?.params,
category_detail: true,
location_detail: true
}
}}
/>
<ImporterDrawer
sessionId={selectedSession ?? -1}
opened={selectedSession != undefined && importOpened}
onClose={() => {
setSelectedSession(undefined);
setImportOpened(false);
table.refreshTable();
}}
/>
</>
);
}

View File

@@ -35,6 +35,7 @@ export function PartVariantTable({ part }: Readonly<{ part: any }>) {
return (
<PartListTable
enableImport={false}
props={{
enableDownload: false,
tableFilters: tableFilters,

View File

@@ -653,6 +653,7 @@ test('Parts - Duplicate', async ({ browser }) => {
// Open "duplicate part" dialog
await page.getByLabel('action-menu-part-actions').click();
await page.getByLabel('action-menu-part-actions-duplicate').click();
// Check for expected fields
@@ -686,9 +687,22 @@ test('Parts - Import supplier part', async ({ browser }) => {
await page.reload();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
await page.getByRole('button', { name: 'action-button-import-part' }).click();
// Open "Add parts" menu
await page.getByRole('button', { name: 'action-menu-add-parts' }).click();
await page
.getByRole('menuitem', { name: 'action-menu-add-parts-create-part' })
.waitFor();
await page
.getByRole('menuitem', { name: 'action-menu-add-parts-import-from-file' })
.waitFor();
await page
.getByRole('menuitem', {
name: 'action-menu-add-parts-import-from-supplier'
})
.click();
await page
.getByRole('textbox', { name: 'textbox-search-for-part' })
.fill('M5');