2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-16 09:46:31 +00:00

Import fix 2 (#9992)

* Fix return types

* Add getStatusCodeLabel func

* Fix logic for import session drawer

- Properly re-fetch session data
- Rendering improvements

* Fix icon

* API permission fixes

* Enhanced playwright testing

* Fix playwright tests

* Tweak playwright tests

* Remove unused var

* Tweak playwright tests
This commit is contained in:
Oliver
2025-07-09 23:32:00 +10:00
committed by GitHub
parent 7ff2ca914a
commit c6166d7c4a
10 changed files with 153 additions and 90 deletions

View File

@@ -135,7 +135,7 @@ class DataImportSessionAcceptFields(APIView):
return Response(importer.serializers.DataImportSessionSerializer(session).data)
class DataImportSessionAcceptRows(DataImporterPermission, CreateAPI):
class DataImportSessionAcceptRows(DataImporterPermissionMixin, CreateAPI):
"""API endpoint to accept the rows for a DataImportSession."""
queryset = importer.models.DataImportSession.objects.all()
@@ -174,7 +174,7 @@ class DataImportColumnMappingDetail(DataImporterPermissionMixin, RetrieveUpdateA
serializer_class = importer.serializers.DataImportColumnMapSerializer
class DataImportRowList(DataImporterPermission, BulkDeleteMixin, ListAPI):
class DataImportRowList(DataImporterPermissionMixin, BulkDeleteMixin, ListAPI):
"""API endpoint for accessing a list of DataImportRow objects."""
queryset = importer.models.DataImportRow.objects.all()
@@ -189,7 +189,7 @@ class DataImportRowList(DataImporterPermission, BulkDeleteMixin, ListAPI):
ordering = 'row_index'
class DataImportRowDetail(DataImporterPermission, RetrieveUpdateDestroyAPI):
class DataImportRowDetail(DataImporterPermissionMixin, RetrieveUpdateDestroyAPI):
"""Detail endpoint for a single DataImportRow object."""
queryset = importer.models.DataImportRow.objects.all()

View File

@@ -5,15 +5,12 @@ import {
Divider,
Drawer,
Group,
Loader,
LoadingOverlay,
Paper,
Space,
Stack,
Stepper,
Text
Stepper
} from '@mantine/core';
import { IconCheck } from '@tabler/icons-react';
import { IconCheck, IconExclamationCircle } from '@tabler/icons-react';
import { type ReactNode, useMemo } from 'react';
import { ModelType } from '@lib/enums/ModelType';
@@ -22,7 +19,7 @@ import useStatusCodes from '../../hooks/UseStatusCodes';
import { StylishText } from '../items/StylishText';
import ImporterDataSelector from './ImportDataSelector';
import ImporterColumnSelector from './ImporterColumnSelector';
import ImporterImportProgress from './ImporterImportProgress';
import ImporterStatus from './ImporterStatus';
/*
* Stepper component showing the current step of the data import process.
@@ -86,17 +83,17 @@ export default function ImporterDrawer({
}, [session.status]);
const widget = useMemo(() => {
if (session.sessionQuery.isLoading || session.sessionQuery.isFetching) {
return <Loader />;
if (session.sessionQuery.isError) {
return (
<Alert color='red' title={t`Error`} icon={<IconExclamationCircle />}>
{t`Failed to fetch import session data`}
</Alert>
);
}
switch (session.status) {
case importSessionStatus.INITIAL:
return <Text>Initial : TODO</Text>;
case importSessionStatus.MAPPING:
return <ImporterColumnSelector session={session} />;
case importSessionStatus.IMPORTING:
return <ImporterImportProgress session={session} />;
case importSessionStatus.PROCESSING:
return <ImporterDataSelector session={session} />;
case importSessionStatus.COMPLETE:
@@ -113,14 +110,7 @@ export default function ImporterDrawer({
</Stack>
);
default:
return (
<Stack gap='xs'>
<Alert color='red' title={t`Unknown Status`} icon={<IconCheck />}>
{t`Import session has unknown status`}: {session.status}
</Alert>
<Button color='red' onClick={onClose}>{t`Close`}</Button>
</Stack>
);
return <ImporterStatus session={session} />;
}
}, [session.status, session.sessionQuery]);
@@ -165,8 +155,7 @@ export default function ImporterDrawer({
}}
>
<Stack gap='xs'>
<LoadingOverlay visible={session.sessionQuery.isFetching} />
<Paper p='md'>{session.sessionQuery.isFetching || widget}</Paper>
<Paper p='md'>{widget}</Paper>
</Stack>
</Drawer>
);

View File

@@ -1,45 +0,0 @@
import { t } from '@lingui/core/macro';
import { Center, Container, Loader, Stack, Text } from '@mantine/core';
import { useInterval } from '@mantine/hooks';
import { useEffect } from 'react';
import { ModelType } from '@lib/enums/ModelType';
import type { ImportSessionState } from '../../hooks/UseImportSession';
import useStatusCodes from '../../hooks/UseStatusCodes';
import { StylishText } from '../items/StylishText';
export default function ImporterImportProgress({
session
}: Readonly<{
session: ImportSessionState;
}>) {
const importSessionStatus = useStatusCodes({
modelType: ModelType.importsession
});
// Periodically refresh the import session data
const interval = useInterval(() => {
if (session.status == importSessionStatus.IMPORTING) {
session.refreshSession();
}
}, 1000);
useEffect(() => {
interval.start();
return interval.stop;
}, []);
return (
<Center>
<Container>
<Stack gap='xs'>
<StylishText size='lg'>{t`Importing Records`}</StylishText>
<Loader />
<Text size='lg'>
{t`Imported Rows`}: {session.sessionData.row_count}
</Text>
</Stack>
</Container>
</Center>
);
}

View File

@@ -0,0 +1,42 @@
import { t } from '@lingui/core/macro';
import { Center, Loader, Stack } from '@mantine/core';
import { useInterval } from '@mantine/hooks';
import { useMemo } from 'react';
import { ModelType } from '@lib/enums/ModelType';
import type { ImportSessionState } from '../../hooks/UseImportSession';
import { StylishText } from '../items/StylishText';
import { getStatusCodeLabel } from '../render/StatusRenderer';
export default function ImporterStatus({
session
}: Readonly<{
session: ImportSessionState;
}>) {
const statusText = useMemo(() => {
return (
getStatusCodeLabel(ModelType.importsession, session.status) ||
t`Unknown Status`
);
}, [session.status]);
// Periodically refresh the import session data
const _interval = useInterval(
() => {
session.refreshSession();
},
1000,
{
autoInvoke: true
}
);
return (
<Center style={{ height: '100%' }}>
<Stack gap='xs' align='center' justify='center'>
<StylishText size='lg'>{statusText}</StylishText>
<Loader />
</Stack>
</Center>
);
}

View File

@@ -132,6 +132,29 @@ export function getStatusCodeName(
return null;
}
/*
* Return the human-readable label for a status code
*/
export function getStatusCodeLabel(
type: ModelType | string,
key: string | number
): string | null {
const statusCodes = getStatusCodes(type);
if (!statusCodes) {
return null;
}
for (const name in statusCodes.values) {
const entry: StatusCodeInterface = statusCodes.values[name];
if (entry.key == key) {
return entry.label;
}
}
return null;
}
/*
* Render the status for a object.
* Uses the values specified in "status_codes.py"

View File

@@ -1,7 +1,8 @@
import { useCallback, useMemo } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { ModelType } from '@lib/enums/ModelType';
import type { UseQueryResult } from '@tanstack/react-query';
import { useInstance } from './UseInstance';
import useStatusCodes from './UseStatusCodes';
@@ -14,7 +15,7 @@ export type ImportSessionState = {
sessionData: any;
setSessionData: (data: any) => void;
refreshSession: () => void;
sessionQuery: any;
sessionQuery: UseQueryResult;
status: number;
availableFields: Record<string, any>;
availableColumns: string[];
@@ -52,10 +53,19 @@ export function useImportSession({
modelType: ModelType.importsession
});
// Current step of the import process
const status: number = useMemo(() => {
return sessionData?.status ?? importSessionStatus.INITIAL;
}, [sessionData, importSessionStatus]);
// Session status (we update whenever the session data changes)
const [status, setStatus] = useState<number>(0);
// Reset the status when the sessionId changes
useEffect(() => {
setStatus(0);
}, [sessionId]);
useEffect(() => {
if (!!sessionData.status && sessionData.status !== status) {
setStatus(sessionData.status);
}
}, [sessionData?.status]);
// List of available writeable database field definitions
const availableFields: any[] = useMemo(() => {

View File

@@ -1,4 +1,8 @@
import { type QueryObserverResult, useQuery } from '@tanstack/react-query';
import {
type QueryObserverResult,
type UseQueryResult,
useQuery
} from '@tanstack/react-query';
import { useCallback, useMemo, useState } from 'react';
import type { ApiEndpoints } from '@lib/enums/ApiEndpoints';
@@ -11,7 +15,7 @@ export interface UseInstanceResult {
setInstance: (instance: any) => void;
refreshInstance: () => void;
refreshInstancePromise: () => Promise<QueryObserverResult<any, any>>;
instanceQuery: any;
instanceQuery: UseQueryResult;
isLoaded: boolean;
}

View File

@@ -1,6 +1,7 @@
import { test } from '../baseFixtures';
import {
clearTableFilters,
clickOnRowMenu,
getRowFromCell,
loadTab,
navigate
@@ -89,17 +90,15 @@ test('Parts - BOM', async ({ browser }) => {
name: 'Small plastic enclosure, black',
exact: true
});
await cell.click({ button: 'right' });
await clickOnRowMenu(cell);
// Check for expected context menu actions
await page.getByRole('button', { name: 'Edit', exact: true }).waitFor();
await page.getByRole('button', { name: 'Delete', exact: true }).waitFor();
await page
.getByRole('button', { name: 'View details', exact: true })
.waitFor();
await page.getByRole('menuitem', { name: 'Edit', exact: true }).waitFor();
await page.getByRole('menuitem', { name: 'Delete', exact: true }).waitFor();
await page
.getByRole('button', { name: 'Edit Substitutes', exact: true })
.getByRole('menuitem', { name: 'Edit Substitutes', exact: true })
.click();
await page.getByText('Edit BOM Substitutes').waitFor();
await page.getByText('1551ACLR').first().waitFor();

View File

@@ -50,13 +50,55 @@ test('Importing - BOM', async ({ browser }) => {
await page.getByText('Mapping data columns to database fields').waitFor();
await page.getByRole('button', { name: 'Accept Column Mapping' }).click();
await page.waitForTimeout(500);
await page.getByText('Processing Data').waitFor();
await page.getByText('Importing Data').waitFor();
await page.getByText('0 / 4').waitFor();
await page.getByText('Torx head screw, M3 thread, 10.0mm').first().waitFor();
await page.getByText('Small plastic enclosure, black').first().waitFor();
// Select some rows
await page
.getByLabel('Importing DataUpload FileMap')
.getByText('002.01-PCBA | Widget Board')
.waitFor();
.getByRole('row', { name: 'Select record 1 0 Thumbnail' })
.getByLabel('Select record')
.click();
await page
.getByRole('row', { name: 'Select record 2 1 Thumbnail' })
.getByLabel('Select record')
.click();
// Delete selected rows
await page
.getByRole('dialog', { name: 'Importing Data Upload File 2' })
.getByLabel('action-button-delete-selected')
.click();
await page.getByRole('button', { name: 'Delete', exact: true }).click();
await page.getByText('Success', { exact: true }).waitFor();
await page.getByText('Items deleted', { exact: true }).waitFor();
// Edit a row
await page
.getByRole('row', { name: 'Select record 1 2 Thumbnail' })
.getByLabel('row-action-menu-')
.click();
await page.getByRole('menuitem', { name: 'Edit' }).click();
await page.getByRole('textbox', { name: 'number-field-quantity' }).fill('12');
await page.waitForTimeout(250);
await page.getByRole('button', { name: 'Submit' }).click();
await page.waitForTimeout(250);
await page.getByText('0 / 2', { exact: true }).waitFor();
// Submit a row
await page
.getByRole('row', { name: 'Select record 1 2 Thumbnail' })
.getByLabel('row-action-menu-')
.click();
await page.getByRole('menuitem', { name: 'Accept' }).click();
await page.getByText('1 / 2', { exact: true }).waitFor();
});
test('Importing - Purchase Order', async ({ browser }) => {

View File

@@ -23,7 +23,6 @@ const OUTPUT_DIR = '../../src/backend/InvenTree/web/static/web';
export default defineConfig(({ command, mode }) => {
// In 'build' mode, we want to use an empty base URL (for static file generation)
const baseUrl: string | undefined = command === 'build' ? '' : undefined;
console.log(`Running Vite in '${command}' mode -> base URL: ${baseUrl}`);
return {
plugins: [