2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-04-15 07:48:51 +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

@@ -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
});