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:
@@ -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()
|
||||
|
@@ -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>
|
||||
);
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
42
src/frontend/src/components/importer/ImporterStatus.tsx
Normal file
42
src/frontend/src/components/importer/ImporterStatus.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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"
|
||||
|
@@ -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(() => {
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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();
|
||||
|
@@ -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 }) => {
|
||||
|
@@ -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: [
|
||||
|
Reference in New Issue
Block a user