2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-05-17 23:08:28 +00:00

feat(frontend): custom model render registration (#11928)

* refactor RenderInstance

* add custom mode renderer

* add optional custom_model api form arg

* switch to fallback mechanism

* remove warning

* fix imports

* adress sonarque rules
This commit is contained in:
Matthias Mair
2026-05-14 05:18:52 +02:00
committed by GitHub
parent 6e2a867c92
commit 701a788a6e
7 changed files with 79 additions and 9 deletions
+7
View File
@@ -0,0 +1,7 @@
import type { ReactNode } from 'react';
import type { InstanceRenderInterface } from '../types/Rendering';
export type setRenderProps = (
model: string,
renderer: (props: Readonly<InstanceRenderInterface>) => ReactNode
) => void;
+2
View File
@@ -45,6 +45,7 @@ export type ApiFormFieldHeader = {
* @param api_url : The API endpoint to fetch data from (for related fields)
* @param pk_field : The primary key field for the related field (default = "pk")
* @param model : The model to use for related fields
* @param custom_model : Optional custom model name (plugins may register renderers for custom models)
* @param modelRenderer : Optional function to render the related model instance (for related fields)
* @param filters : Optional API filters to apply to related fields
* @param child: Optional definition of a child field (for nested objects)
@@ -101,6 +102,7 @@ export type ApiFormFieldType = {
api_url?: string;
pk_field?: string;
model?: ModelType;
custom_model?: string;
modelRenderer?: (instance: any) => ReactNode;
filters?: any;
child?: ApiFormFieldType;
+4
View File
@@ -6,6 +6,7 @@ import type { JSX } from 'react';
import type { NavigateFunction } from 'react-router-dom';
import type { ModelDict } from '../enums/ModelInformation';
import type { ModelType } from '../enums/ModelType';
import type { setRenderProps } from '../states/types';
import type {
ApiFormModalProps,
ApiFormProps,
@@ -119,6 +120,9 @@ export type InvenTreePluginContext = {
theme: MantineTheme;
colorScheme: MantineColorScheme;
forms: InvenTreeFormsContext;
stateFnc: {
setRenderer: setRenderProps;
};
tables: InvenTreeTablesContext<any>;
importer: ImporterDrawerContext;
model?: ModelType | string;
@@ -384,10 +384,14 @@ export function RelatedModelField({
}
return (
<RenderInstance instance={data} model={definition.model ?? undefined} />
<RenderInstance
instance={data}
model={definition.model ?? undefined}
custom_model={definition.custom_model ?? undefined}
/>
);
},
[definition.model, definition.modelRenderer]
[definition.model, definition.modelRenderer, definition.custom_model]
);
// Update form values when the selected value changes
@@ -45,6 +45,7 @@ import {
getGlobalImporterState,
openGlobalImporter
} from '../../states/ImporterState';
import { usePluginState } from '../../states/PluginState';
import { useServerApiState } from '../../states/ServerApiState';
import { InvenTreeTableInternal } from '../../tables/InvenTreeTable';
import { EditApiForm } from '../forms/ApiForm';
@@ -55,6 +56,7 @@ import { RenderInlineModel } from '../render/Instance';
export const useInvenTreeContext = () => {
const [locale, host] = useLocalState(useShallow((s) => [s.language, s.host]));
const [server] = useServerApiState(useShallow((s) => [s.server]));
const [setRenderer] = usePluginState(useShallow((s) => [s.setRenderer]));
const navigate = useNavigate();
const user = useUserState();
const { colorScheme } = useMantineColorScheme();
@@ -120,6 +122,9 @@ export const useInvenTreeContext = () => {
transferStock: useTransferStockItem,
returnStock: useReturnStockItem
}
},
stateFnc: {
setRenderer: setRenderer
}
};
}, [
@@ -16,6 +16,7 @@ import { ModelInformationDict } from '@lib/enums/ModelInformation';
import { ModelType } from '@lib/enums/ModelType';
import { apiUrl } from '@lib/functions/Api';
import type {
InstanceRenderInterface,
ModelRendererDict,
RemoteInstanceProps,
RenderInlineModelProps,
@@ -25,6 +26,7 @@ import type {
export type { InstanceRenderInterface } from '@lib/types/Rendering';
import { getBaseUrl, navigateToLink, shortenString } from '@lib/index';
import { useApi } from '../../contexts/ApiContext';
import { usePluginState } from '../../states/PluginState';
import { Thumbnail } from '../images/Thumbnail';
import { RenderBuildItem, RenderBuildLine, RenderBuildOrder } from './Build';
import {
@@ -106,18 +108,23 @@ export const RendererLookup: ModelRendererDict = {
* Render an instance of a database model, depending on the provided data
*/
export function RenderInstance(props: RenderInstanceProps): ReactNode {
if (props.model === undefined) {
return <UnknownRenderer model={props.model} />;
let RenderComponent:
| ((props: Readonly<InstanceRenderInterface>) => ReactNode)
| undefined;
// core model renderer
if (props.model !== undefined && props.custom_model === undefined) {
RenderComponent =
RendererLookup[props.model.toString().toLowerCase() as ModelType];
}
// custom model renderer (registered by a plugin) as a fallback to the core model renderer
RenderComponent ??= usePluginState().getRenderer(
props.custom_model ?? props.model ?? ''
);
const model_name = props.model.toString().toLowerCase() as ModelType;
const RenderComponent = RendererLookup[model_name];
// provider component
if (!RenderComponent) {
return <UnknownRenderer model={props.model} />;
}
return <RenderComponent {...props} />;
}
+41
View File
@@ -0,0 +1,41 @@
import type { setRenderProps } from '@lib/states/types';
import type { InstanceRenderInterface } from '@lib/types/Rendering';
import type { ReactNode } from 'react';
import { create } from 'zustand';
interface PluginStateProps {
customRenders: Record<
string,
(props: Readonly<InstanceRenderInterface>) => ReactNode
>;
getRenderer: (
model: string
) => ((props: Readonly<InstanceRenderInterface>) => ReactNode) | undefined;
setRenderer: setRenderProps;
}
/**
* Global state manager for handling plugin-provided states, such as custom renderers for models.
* This allows plugins to register custom renderers for specific models
*/
export const usePluginState = create<PluginStateProps>()((set, get) => ({
customRenders: {},
getRenderer: (model: string) => {
return get().customRenders[model] || undefined;
},
setRenderer: (
model: string,
renderer: (props: Readonly<InstanceRenderInterface>) => ReactNode
) => {
// ensure model is not already registered
if (get().customRenders[model]) {
return;
}
set((state) => ({
customRenders: {
...state.customRenders,
[model]: renderer
}
}));
}
}));