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:
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}));
|
||||
}
|
||||
}));
|
||||
Reference in New Issue
Block a user