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:
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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[] => {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user