2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-04-12 14:28:55 +00:00

[UI] Pass custom fields through to the importer session (#11688)

* [UI] Pass custom fields through to the importer session

* Support custom model rendering within the data import wizard

* Update CHANGELOG.md

* Update UI version
This commit is contained in:
Oliver
2026-04-08 23:50:16 +10:00
committed by GitHub
parent b9a66da833
commit cc77d1d5e6
9 changed files with 96 additions and 18 deletions

View File

@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- [#11685](https://github.com/inventree/InvenTree/pull/11685) exposes the data importer wizard to the plugin interface, allowing plugins to trigger the data importer wizard and perform custom data imports from the UI.
- [#11692](https://github.com/inventree/InvenTree/pull/11692) adds line item numbering for external orders (purchase, sales and return orders). This allows users to specify a line number for each line item on the order, which can be used for reference purposes. The line number is optional, and can be left blank if not required. The line number is stored as a string, to allow for more flexible formatting (e.g. "1", "1.1", "A", etc).
- [#11641](https://github.com/inventree/InvenTree/pull/11641) adds support for custom parameters against the SalesOrderShipment model.
- [#11527](https://github.com/inventree/InvenTree/pull/11527) adds a new API endpoint for monitoring the status of a particular background task. This endpoint allows clients to check the status of a background task and receive updates when the task is complete. This is useful for long-running tasks that may take some time to complete, allowing clients to provide feedback to users about the progress of the task.

View File

@@ -2,6 +2,10 @@
This file contains historical changelog information for the InvenTree UI components library.
### 0.10.1 - April 2026
Allows plugins to specify custom model rendering functions within the data import wizard, allowing import of data models not defined in the core InvenTree codebase.
### 0.10.0 - April 2026
Exposes the `importer` object to the plugin context, allow plugins to initialize a data import session using the data importer wizard.

View File

@@ -1,7 +1,7 @@
{
"name": "@inventreedb/ui",
"description": "UI components for the InvenTree project",
"version": "0.10.0",
"version": "0.10.1",
"private": false,
"type": "module",
"license": "MIT",

View File

@@ -4,6 +4,7 @@ import ImporterDrawer from './ImporterDrawer';
export default function GlobalImporterDrawer() {
const isOpen = useImporterState((state) => state.isOpen);
const sessionId = useImporterState((state) => state.sessionId);
const customFields = useImporterState((state) => state.customFields);
const closeImporter = useImporterState((state) => state.closeImporter);
if (!isOpen || sessionId === null) {
@@ -15,6 +16,7 @@ export default function GlobalImporterDrawer() {
sessionId={sessionId}
opened={isOpen}
onClose={closeImporter}
customFields={customFields}
/>
);
}

View File

@@ -36,10 +36,12 @@ import { RenderRemoteInstance } from '../render/Instance';
function ImporterDataCell({
session,
column,
fieldDef,
row,
onEdit
}: Readonly<{
session: ImportSessionState;
fieldDef: any;
column: any;
row: any;
onEdit?: () => void;
@@ -63,22 +65,24 @@ function ImporterDataCell({
}, [row.errors, column.field]);
const cellValue: ReactNode = useMemo(() => {
const field_def = session.availableFields[column.field];
// const field_def = session.availableFields[column.field];
if (!row?.data) {
return '-';
}
switch (field_def?.type) {
switch (fieldDef?.type) {
case 'boolean':
return (
<YesNoButton value={row.data ? row.data[column.field] : false} />
);
case 'related field':
if (field_def.model && row.data[column.field]) {
if (fieldDef.model && row.data[column.field]) {
return (
<RenderRemoteInstance
model={field_def.model}
model={fieldDef.model}
modelRenderer={fieldDef.modelRenderer}
modelUrl={fieldDef.api_url}
pk={row.data[column.field]}
/>
);
@@ -95,7 +99,7 @@ function ImporterDataCell({
}
return value;
}, [row.data, column.field, session.availableFields]);
}, [fieldDef, row.data, column.field, session.availableFields]);
const cellValid: boolean = useMemo(
() => cellErrors.length == 0,
@@ -127,9 +131,11 @@ function ImporterDataCell({
}
export default function ImporterDataSelector({
session
session,
customFields
}: Readonly<{
session: ImportSessionState;
customFields?: ApiFormFieldSet | null;
}>) {
const api = useApi();
const table = useTable('dataimporter');
@@ -142,9 +148,11 @@ export default function ImporterDataSelector({
for (const field of selectedFieldNames) {
// Find the field definition in session.availableFields
const fieldDef = session.availableFields[field];
if (fieldDef) {
const customField = customFields?.[field] ?? null;
if (fieldDef || customField) {
// Construct field filters based on session field filters
let filters = fieldDef.filters ?? {};
let filters = fieldDef?.filters ?? {};
if (session.fieldFilters[field]) {
filters = {
@@ -159,15 +167,30 @@ export default function ImporterDataSelector({
fields[field] = {
...fieldDef,
...customField,
field_type: fieldDef.type,
description: fieldDef.help_text,
filters: filters
};
console.log('Defined Field:', field);
console.log({
...fieldDef,
...customField,
field_type: fieldDef.type,
description: fieldDef.help_text,
filters: filters
});
}
}
return fields;
}, [selectedFieldNames, session.availableFields, session.fieldFilters]);
}, [
customFields,
selectedFieldNames,
session.availableFields,
session.fieldFilters
]);
const importData = useCallback(
(rows: number[]) => {
@@ -322,6 +345,10 @@ export default function ImporterDataSelector({
column={column}
row={row}
onEdit={() => editCell(row, column)}
fieldDef={{
...session.availableFields[column.field],
...customFields?.[column.field]
}}
/>
);
}
@@ -330,7 +357,7 @@ export default function ImporterDataSelector({
];
return columns;
}, [session]);
}, [session, customFields]);
const rowActions = useCallback(
(record: any): RowAction[] => {

View File

@@ -15,7 +15,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { apiUrl } from '@lib/functions/Api';
import type { ApiFormFieldType } from '@lib/types/Forms';
import type { ApiFormFieldSet, ApiFormFieldType } from '@lib/types/Forms';
import { useDebouncedValue } from '@mantine/hooks';
import { useApi } from '../../contexts/ApiContext';
import type { ImportSessionState } from '../../hooks/UseImportSession';
@@ -77,9 +77,11 @@ function ImporterColumn({
function ImporterDefaultField({
fieldName,
customField,
session
}: {
fieldName: string;
customField?: ApiFormFieldType | null;
session: ImportSessionState;
}) {
const api = useApi();
@@ -162,8 +164,15 @@ function ImporterDefaultField({
};
}
if (customField) {
def = {
...def,
...customField
};
}
return def;
}, [fieldName, session.availableFields, session.fieldDefaults]);
}, [fieldName, session.availableFields, session.fieldDefaults, customField]);
return (
fieldDef && <StandaloneField fieldDefinition={fieldDef} hideLabels={true} />
@@ -173,10 +182,12 @@ function ImporterDefaultField({
function ImporterColumnTableRow({
session,
column,
customField,
options
}: Readonly<{
session: ImportSessionState;
column: any;
customField?: ApiFormFieldType | null;
options: any;
}>) {
return (
@@ -200,16 +211,22 @@ function ImporterColumnTableRow({
<ImporterColumn column={column} options={options} />
</Table.Td>
<Table.Td>
<ImporterDefaultField fieldName={column.field} session={session} />
<ImporterDefaultField
fieldName={column.field}
session={session}
customField={customField}
/>
</Table.Td>
</Table.Tr>
);
}
export default function ImporterColumnSelector({
session
session,
customFields
}: Readonly<{
session: ImportSessionState;
customFields?: ApiFormFieldSet | null;
}>) {
const api = useApi();
@@ -279,6 +296,7 @@ export default function ImporterColumnSelector({
session={session}
column={column}
options={columnOptions}
customField={customFields?.[column.field] || null}
/>
);
})}

View File

@@ -14,6 +14,7 @@ import { IconCheck, IconExclamationCircle } from '@tabler/icons-react';
import { type ReactNode, useMemo } from 'react';
import { ModelType } from '@lib/enums/ModelType';
import type { ApiFormFieldSet } from '@lib/index';
import { useImportSession } from '../../hooks/UseImportSession';
import useStatusCodes from '../../hooks/UseStatusCodes';
import { StylishText } from '../items/StylishText';
@@ -51,10 +52,12 @@ function ImportDrawerStepper({
export default function ImporterDrawer({
sessionId,
customFields,
opened,
onClose
}: Readonly<{
sessionId: number;
customFields?: ApiFormFieldSet | null;
opened: boolean;
onClose: () => void;
}>) {
@@ -93,9 +96,16 @@ export default function ImporterDrawer({
switch (session.status) {
case importSessionStatus.MAPPING:
return <ImporterColumnSelector session={session} />;
return (
<ImporterColumnSelector
session={session}
customFields={customFields}
/>
);
case importSessionStatus.PROCESSING:
return <ImporterDataSelector session={session} />;
return (
<ImporterDataSelector session={session} customFields={customFields} />
);
case importSessionStatus.COMPLETE:
return (
<Stack gap='xs'>

View File

@@ -119,9 +119,13 @@ export function RenderInstance(props: RenderInstanceProps): ReactNode {
export function RenderRemoteInstance({
model,
modelUrl,
modelRenderer,
pk
}: Readonly<{
model: ModelType;
modelUrl?: string;
modelRenderer?: (instance: any) => ReactNode;
pk: number;
}>): ReactNode {
const api = useApi();
@@ -129,7 +133,9 @@ export function RenderRemoteInstance({
const { data, isLoading, isFetching } = useQuery({
queryKey: ['model', model, pk],
queryFn: async () => {
const url = apiUrl(ModelInformationDict[model].api_endpoint, pk);
const url = modelUrl
? apiUrl(modelUrl, pk)
: apiUrl(ModelInformationDict[model].api_endpoint, pk);
return api.get(url).then((response) => response.data);
}
@@ -147,6 +153,10 @@ export function RenderRemoteInstance({
);
}
if (!!modelRenderer) {
return modelRenderer({ instance: data });
}
return <RenderInstance model={model} instance={data} />;
}

View File

@@ -6,15 +6,18 @@
* The `useImporterState` hook can be used to access and manipulate the importer state from any component, while the `openGlobalImporter` and `closeGlobalImporter` functions provide convenient ways to control the importer from outside of React components.
*/
import type { ApiFormFieldSet } from '@lib/index';
import { create } from 'zustand';
export interface ImporterOpenOptions {
fields?: ApiFormFieldSet | null;
onClose?: () => void;
}
interface ImporterStateProps {
isOpen: boolean;
sessionId: number | null;
customFields?: ApiFormFieldSet | null;
onCloseCallback?: () => void;
openImporter: (sessionId: number, options?: ImporterOpenOptions) => void;
closeImporter: () => void;
@@ -23,12 +26,14 @@ interface ImporterStateProps {
export const useImporterState = create<ImporterStateProps>()((set, get) => ({
isOpen: false,
sessionId: null,
customFields: null,
onCloseCallback: undefined,
openImporter: (sessionId: number, options?: ImporterOpenOptions) => {
set({
sessionId,
isOpen: true,
customFields: options?.fields ?? null,
onCloseCallback: options?.onClose
});
},
@@ -39,6 +44,7 @@ export const useImporterState = create<ImporterStateProps>()((set, get) => ({
set({
sessionId: null,
isOpen: false,
customFields: null,
onCloseCallback: undefined
});