diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dcdf7ce75..fe386a040e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/frontend/CHANGELOG.md b/src/frontend/CHANGELOG.md index 188953ee63..7fd7be38d5 100644 --- a/src/frontend/CHANGELOG.md +++ b/src/frontend/CHANGELOG.md @@ -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. diff --git a/src/frontend/package.json b/src/frontend/package.json index 6c43c020f4..06802a171e 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -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", diff --git a/src/frontend/src/components/importer/GlobalImporterDrawer.tsx b/src/frontend/src/components/importer/GlobalImporterDrawer.tsx index de34229b14..de4a5a7427 100644 --- a/src/frontend/src/components/importer/GlobalImporterDrawer.tsx +++ b/src/frontend/src/components/importer/GlobalImporterDrawer.tsx @@ -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} /> ); } diff --git a/src/frontend/src/components/importer/ImportDataSelector.tsx b/src/frontend/src/components/importer/ImportDataSelector.tsx index d91a94f776..0b09bf31e6 100644 --- a/src/frontend/src/components/importer/ImportDataSelector.tsx +++ b/src/frontend/src/components/importer/ImportDataSelector.tsx @@ -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 ( ); case 'related field': - if (field_def.model && row.data[column.field]) { + if (fieldDef.model && row.data[column.field]) { return ( ); @@ -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[] => { diff --git a/src/frontend/src/components/importer/ImporterColumnSelector.tsx b/src/frontend/src/components/importer/ImporterColumnSelector.tsx index 8031495cac..83b420e812 100644 --- a/src/frontend/src/components/importer/ImporterColumnSelector.tsx +++ b/src/frontend/src/components/importer/ImporterColumnSelector.tsx @@ -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 && @@ -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({ - + ); } 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} /> ); })} diff --git a/src/frontend/src/components/importer/ImporterDrawer.tsx b/src/frontend/src/components/importer/ImporterDrawer.tsx index eb5a4343af..624c7067fb 100644 --- a/src/frontend/src/components/importer/ImporterDrawer.tsx +++ b/src/frontend/src/components/importer/ImporterDrawer.tsx @@ -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 ; + return ( + + ); case importSessionStatus.PROCESSING: - return ; + return ( + + ); case importSessionStatus.COMPLETE: return ( diff --git a/src/frontend/src/components/render/Instance.tsx b/src/frontend/src/components/render/Instance.tsx index a3174e1c58..933a2c7f52 100644 --- a/src/frontend/src/components/render/Instance.tsx +++ b/src/frontend/src/components/render/Instance.tsx @@ -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 ; } diff --git a/src/frontend/src/states/ImporterState.tsx b/src/frontend/src/states/ImporterState.tsx index 910970da66..b7e3faedca 100644 --- a/src/frontend/src/states/ImporterState.tsx +++ b/src/frontend/src/states/ImporterState.tsx @@ -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()((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()((set, get) => ({ set({ sessionId: null, isOpen: false, + customFields: null, onCloseCallback: undefined });