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:
@@ -188,6 +188,7 @@ export function PrintingActions({
|
||||
<ActionDropdown
|
||||
tooltip={t`Printing Actions`}
|
||||
icon={<IconPrinter />}
|
||||
position='bottom-start'
|
||||
disabled={!enabled}
|
||||
actions={[
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export function PartVariantTable({ part }: Readonly<{ part: any }>) {
|
||||
|
||||
return (
|
||||
<PartListTable
|
||||
enableImport={false}
|
||||
props={{
|
||||
enableDownload: false,
|
||||
tableFilters: tableFilters,
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user