diff --git a/src/frontend/lib/states/types.tsx b/src/frontend/lib/states/types.tsx new file mode 100644 index 0000000000..475ab0fd13 --- /dev/null +++ b/src/frontend/lib/states/types.tsx @@ -0,0 +1,7 @@ +import type { ReactNode } from 'react'; +import type { InstanceRenderInterface } from '../types/Rendering'; + +export type setRenderProps = ( + model: string, + renderer: (props: Readonly) => ReactNode +) => void; diff --git a/src/frontend/lib/types/Forms.tsx b/src/frontend/lib/types/Forms.tsx index 44e37a2b2f..83e78a8e1a 100644 --- a/src/frontend/lib/types/Forms.tsx +++ b/src/frontend/lib/types/Forms.tsx @@ -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; diff --git a/src/frontend/lib/types/Plugins.tsx b/src/frontend/lib/types/Plugins.tsx index 0628eda9e9..2038b1f297 100644 --- a/src/frontend/lib/types/Plugins.tsx +++ b/src/frontend/lib/types/Plugins.tsx @@ -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; importer: ImporterDrawerContext; model?: ModelType | string; diff --git a/src/frontend/src/components/forms/fields/RelatedModelField.tsx b/src/frontend/src/components/forms/fields/RelatedModelField.tsx index e04ea1562a..259fab72f2 100644 --- a/src/frontend/src/components/forms/fields/RelatedModelField.tsx +++ b/src/frontend/src/components/forms/fields/RelatedModelField.tsx @@ -384,10 +384,14 @@ export function RelatedModelField({ } return ( - + ); }, - [definition.model, definition.modelRenderer] + [definition.model, definition.modelRenderer, definition.custom_model] ); // Update form values when the selected value changes diff --git a/src/frontend/src/components/plugins/PluginContext.tsx b/src/frontend/src/components/plugins/PluginContext.tsx index a39de16bb6..e6790c4c49 100644 --- a/src/frontend/src/components/plugins/PluginContext.tsx +++ b/src/frontend/src/components/plugins/PluginContext.tsx @@ -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 } }; }, [ diff --git a/src/frontend/src/components/render/Instance.tsx b/src/frontend/src/components/render/Instance.tsx index dff59e022c..329eb4dd32 100644 --- a/src/frontend/src/components/render/Instance.tsx +++ b/src/frontend/src/components/render/Instance.tsx @@ -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 ; + let RenderComponent: + | ((props: Readonly) => 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 ; } - return ; } diff --git a/src/frontend/src/states/PluginState.tsx b/src/frontend/src/states/PluginState.tsx new file mode 100644 index 0000000000..f0d3bc20d8 --- /dev/null +++ b/src/frontend/src/states/PluginState.tsx @@ -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) => ReactNode + >; + getRenderer: ( + model: string + ) => ((props: Readonly) => 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()((set, get) => ({ + customRenders: {}, + getRenderer: (model: string) => { + return get().customRenders[model] || undefined; + }, + setRenderer: ( + model: string, + renderer: (props: Readonly) => ReactNode + ) => { + // ensure model is not already registered + if (get().customRenders[model]) { + return; + } + set((state) => ({ + customRenders: { + ...state.customRenders, + [model]: renderer + } + })); + } +}));