diff --git a/src/backend/InvenTree/importer/api.py b/src/backend/InvenTree/importer/api.py
index cc00b7635c..bae964131a 100644
--- a/src/backend/InvenTree/importer/api.py
+++ b/src/backend/InvenTree/importer/api.py
@@ -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()
diff --git a/src/frontend/src/components/importer/ImporterDrawer.tsx b/src/frontend/src/components/importer/ImporterDrawer.tsx
index 7213fac500..2ed836627c 100644
--- a/src/frontend/src/components/importer/ImporterDrawer.tsx
+++ b/src/frontend/src/components/importer/ImporterDrawer.tsx
@@ -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 ;
+ if (session.sessionQuery.isError) {
+ return (
+ }>
+ {t`Failed to fetch import session data`}
+
+ );
}
switch (session.status) {
- case importSessionStatus.INITIAL:
- return Initial : TODO;
case importSessionStatus.MAPPING:
return ;
- case importSessionStatus.IMPORTING:
- return ;
case importSessionStatus.PROCESSING:
return ;
case importSessionStatus.COMPLETE:
@@ -113,14 +110,7 @@ export default function ImporterDrawer({
);
default:
- return (
-
- }>
- {t`Import session has unknown status`}: {session.status}
-
-
-
- );
+ return ;
}
}, [session.status, session.sessionQuery]);
@@ -165,8 +155,7 @@ export default function ImporterDrawer({
}}
>
-
- {session.sessionQuery.isFetching || widget}
+ {widget}
);
diff --git a/src/frontend/src/components/importer/ImporterImportProgress.tsx b/src/frontend/src/components/importer/ImporterImportProgress.tsx
deleted file mode 100644
index 3ee15a1a28..0000000000
--- a/src/frontend/src/components/importer/ImporterImportProgress.tsx
+++ /dev/null
@@ -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 (
-
-
-
- {t`Importing Records`}
-
-
- {t`Imported Rows`}: {session.sessionData.row_count}
-
-
-
-
- );
-}
diff --git a/src/frontend/src/components/importer/ImporterStatus.tsx b/src/frontend/src/components/importer/ImporterStatus.tsx
new file mode 100644
index 0000000000..9dbf5d9218
--- /dev/null
+++ b/src/frontend/src/components/importer/ImporterStatus.tsx
@@ -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 (
+
+
+ {statusText}
+
+
+
+ );
+}
diff --git a/src/frontend/src/components/render/StatusRenderer.tsx b/src/frontend/src/components/render/StatusRenderer.tsx
index bb54da9e6e..d7dc8a3599 100644
--- a/src/frontend/src/components/render/StatusRenderer.tsx
+++ b/src/frontend/src/components/render/StatusRenderer.tsx
@@ -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"
diff --git a/src/frontend/src/hooks/UseImportSession.tsx b/src/frontend/src/hooks/UseImportSession.tsx
index dde146f019..7503228c03 100644
--- a/src/frontend/src/hooks/UseImportSession.tsx
+++ b/src/frontend/src/hooks/UseImportSession.tsx
@@ -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;
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(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(() => {
diff --git a/src/frontend/src/hooks/UseInstance.tsx b/src/frontend/src/hooks/UseInstance.tsx
index 2bdf564f1d..34a766f921 100644
--- a/src/frontend/src/hooks/UseInstance.tsx
+++ b/src/frontend/src/hooks/UseInstance.tsx
@@ -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>;
- instanceQuery: any;
+ instanceQuery: UseQueryResult;
isLoaded: boolean;
}
diff --git a/src/frontend/tests/pages/pui_part.spec.ts b/src/frontend/tests/pages/pui_part.spec.ts
index 4a3f037994..994ef8f574 100644
--- a/src/frontend/tests/pages/pui_part.spec.ts
+++ b/src/frontend/tests/pages/pui_part.spec.ts
@@ -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();
diff --git a/src/frontend/tests/pui_importing.spec.ts b/src/frontend/tests/pui_importing.spec.ts
index 6cbe55e69e..4b38ba862f 100644
--- a/src/frontend/tests/pui_importing.spec.ts
+++ b/src/frontend/tests/pui_importing.spec.ts
@@ -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 }) => {
diff --git a/src/frontend/vite.config.ts b/src/frontend/vite.config.ts
index 888cc0b727..610d55a667 100644
--- a/src/frontend/vite.config.ts
+++ b/src/frontend/vite.config.ts
@@ -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: [