diff --git a/src/frontend/src/components/plugins/PluginContext.tsx b/src/frontend/src/components/plugins/PluginContext.tsx index 9a134168d8..ea57e65ef6 100644 --- a/src/frontend/src/components/plugins/PluginContext.tsx +++ b/src/frontend/src/components/plugins/PluginContext.tsx @@ -9,7 +9,6 @@ import { useMemo } from 'react'; import { NavigateFunction, useNavigate } from 'react-router-dom'; import { api } from '../../App'; -import { ModelType } from '../../enums/ModelType'; import { useLocalState } from '../../states/LocalState'; import { SettingsStateProps, @@ -18,13 +17,10 @@ import { } from '../../states/SettingsState'; import { UserStateProps, useUserState } from '../../states/UserState'; -/* +/** * A set of properties which are passed to a plugin, * for rendering an element in the user interface. * - * @param model - The model type for the plugin (e.g. 'part' / 'purchaseorder') - * @param id - The ID (primary key) of the model instance for the plugin - * @param instance - The model instance data (if available) * @param api - The Axios API instance (see ../states/ApiState.tsx) * @param user - The current user instance (see ../states/UserState.tsx) * @param userSettings - The current user settings (see ../states/SettingsState.tsx) @@ -33,10 +29,7 @@ import { UserStateProps, useUserState } from '../../states/UserState'; * @param theme - The current Mantine theme * @param colorScheme - The current Mantine color scheme (e.g. 'light' / 'dark') */ -export type PluginContext = { - model?: ModelType | string; - id?: string | number | null; - instance?: any; +export type InvenTreeContext = { api: AxiosInstance; user: UserStateProps; userSettings: SettingsStateProps; @@ -47,7 +40,7 @@ export type PluginContext = { colorScheme: MantineColorScheme; }; -export const usePluginContext = () => { +export const useInvenTreeContext = () => { const host = useLocalState.getState().host; const navigate = useNavigate(); const user = useUserState(); @@ -56,7 +49,7 @@ export const usePluginContext = () => { const globalSettings = useGlobalSettingsState(); const userSettings = useUserSettingsState(); - const contextData: PluginContext = useMemo(() => { + const contextData = useMemo(() => { return { user: user, host: host, diff --git a/src/frontend/src/components/plugins/PluginPanel.tsx b/src/frontend/src/components/plugins/PluginPanel.tsx index dbabc549b8..153a4cd1ab 100644 --- a/src/frontend/src/components/plugins/PluginPanel.tsx +++ b/src/frontend/src/components/plugins/PluginPanel.tsx @@ -3,7 +3,7 @@ import { Alert, Stack, Text } from '@mantine/core'; import { IconExclamationCircle } from '@tabler/icons-react'; import { ReactNode, useEffect, useRef, useState } from 'react'; -import { PluginContext } from './PluginContext'; +import { InvenTreeContext } from './PluginContext'; import { findExternalPluginFunction } from './PluginSource'; // Definition of the plugin panel properties, provided by the server API @@ -21,7 +21,7 @@ export async function isPluginPanelHidden({ pluginContext }: { pluginProps: PluginPanelProps; - pluginContext: PluginContext; + pluginContext: InvenTreeContext; }): Promise { if (!pluginProps.source) { // No custom source supplied - panel is not hidden @@ -66,7 +66,7 @@ export default function PluginPanelContent({ pluginContext }: Readonly<{ pluginProps: PluginPanelProps; - pluginContext: PluginContext; + pluginContext: InvenTreeContext; }>): ReactNode { const ref = useRef(); diff --git a/src/frontend/src/components/plugins/PluginSource.tsx b/src/frontend/src/components/plugins/PluginSource.tsx index c01f1115ab..6db4640493 100644 --- a/src/frontend/src/components/plugins/PluginSource.tsx +++ b/src/frontend/src/components/plugins/PluginSource.tsx @@ -36,7 +36,7 @@ export async function loadExternalPluginSource(source: string) { export async function findExternalPluginFunction( source: string, functionName: string -) { +): Promise { const module = await loadExternalPluginSource(source); if (module && module[functionName]) { diff --git a/src/frontend/src/components/plugins/PluginUIFeature.tsx b/src/frontend/src/components/plugins/PluginUIFeature.tsx index d074f1a8a9..13c9233483 100644 --- a/src/frontend/src/components/plugins/PluginUIFeature.tsx +++ b/src/frontend/src/components/plugins/PluginUIFeature.tsx @@ -11,36 +11,15 @@ import { import { TemplateI } from '../../tables/settings/TemplateTable'; import { EditorComponent } from '../editors/TemplateEditor/TemplateEditor'; +import { + PluginUIFuncWithoutInvenTreeContextType, + TemplateEditorUIFeature +} from './PluginUIFeatureTypes'; -// Definition of the plugin ui feature properties, provided by the server API -export type PluginUIFeatureProps = ( - | { - feature_type: 'template_editor'; - options: { - title: string; - slug: string; - }; - } - | { - feature_type: 'template_preview'; - options: { - title: string; - slug: string; - }; - } -) & { - source: string; -}; - -export type TemplateEditorRenderContextType = { - registerHandlers: (params: { - setCode: (code: string) => void; - getCode: () => string; - }) => void; - template: TemplateI; -}; - -export const getPluginTemplateEditor = (func: any, template: any) => +export const getPluginTemplateEditor = ( + func: PluginUIFuncWithoutInvenTreeContextType, + template: TemplateI +) => forwardRef((props, ref) => { const elRef = useRef(); const [error, setError] = useState(undefined); @@ -64,7 +43,7 @@ export const getPluginTemplateEditor = (func: any, template: any) => useEffect(() => { (async () => { try { - await func(elRef.current, { + await func(elRef.current!, { registerHandlers: ({ getCode, setCode }) => { setCodeRef.current = setCode; getCodeRef.current = getCode; @@ -74,7 +53,7 @@ export const getPluginTemplateEditor = (func: any, template: any) => } }, template - } as TemplateEditorRenderContextType); + }); } catch (error) { setError(t`Error occurred while rendering the template editor.`); console.error(error); diff --git a/src/frontend/src/components/plugins/PluginUIFeatureTypes.ts b/src/frontend/src/components/plugins/PluginUIFeatureTypes.ts new file mode 100644 index 0000000000..b341bfe2de --- /dev/null +++ b/src/frontend/src/components/plugins/PluginUIFeatureTypes.ts @@ -0,0 +1,67 @@ +import { InvenTreeIconType } from '../../functions/icons'; +import { TemplateI } from '../../tables/settings/TemplateTable'; +import { InvenTreeContext } from './PluginContext'; + +// #region Type Helpers +export type BaseUIFeature = { + featureType: string; + requestContext: Record; + responseOptions: Record; + renderContext: Record; +}; + +export type PluginUIGetFeatureType = ( + ref: HTMLDivElement, + params: { + renderContext: T['renderContext']; + inventreeContext: InvenTreeContext; + } +) => void; + +export type PluginUIFuncWithoutInvenTreeContextType = ( + ref: HTMLDivElement, + renderContext: T['renderContext'] +) => void; + +export type PluginUIFeatureAPIResponse = { + feature_type: T['featureType']; + options: T['responseOptions']; + source: string; +}; + +// #region Types +export type TemplateEditorUIFeature = { + featureType: 'template_editor'; + requestContext: { + template_type: string; + template_model: string; + }; + responseOptions: { + key: string; + title: string; + icon: InvenTreeIconType; + }; + renderContext: { + registerHandlers: (params: { + setCode: (code: string) => void; + getCode: () => string; + }) => void; + template: TemplateI; + }; +}; + +export type TemplatePreviewUIFeature = { + featureType: 'template_preview'; + requestContext: { + template_type: string; + template_model: string; + }; + responseOptions: { + key: string; + title: string; + icon: InvenTreeIconType; + }; + renderContext: { + template: TemplateI; + }; +}; diff --git a/src/frontend/src/hooks/UsePluginPanels.tsx b/src/frontend/src/hooks/UsePluginPanels.tsx index ed9abb1734..3f991f86f9 100644 --- a/src/frontend/src/hooks/UsePluginPanels.tsx +++ b/src/frontend/src/hooks/UsePluginPanels.tsx @@ -4,8 +4,8 @@ import { useEffect, useMemo, useState } from 'react'; import { api } from '../App'; import { PanelType } from '../components/nav/Panel'; import { - PluginContext, - usePluginContext + InvenTreeContext, + useInvenTreeContext } from '../components/plugins/PluginContext'; import PluginPanelContent, { PluginPanelProps, @@ -18,6 +18,17 @@ import { InvenTreeIcon, InvenTreeIconType } from '../functions/icons'; import { apiUrl } from '../states/ApiState'; import { useGlobalSettingsState } from '../states/SettingsState'; +/** + * @param model - The model type for the plugin (e.g. 'part' / 'purchaseorder') + * @param id - The ID (primary key) of the model instance for the plugin + * @param instance - The model instance data (if available) + */ +export type PluginPanelContext = InvenTreeContext & { + model?: ModelType | string; + id?: string | number | null; + instance?: any; +}; + export function usePluginPanels({ instance, model, @@ -59,13 +70,13 @@ export function usePluginPanels({ }); // Cache the context data which is delivered to the plugins - const pluginContext = usePluginContext(); - const contextData: PluginContext = useMemo(() => { + const inventreeContext = useInvenTreeContext(); + const contextData = useMemo(() => { return { model: model, id: id, instance: instance, - ...pluginContext + ...inventreeContext }; }, [model, id, instance]); diff --git a/src/frontend/src/hooks/UsePluginUIFeature.tsx b/src/frontend/src/hooks/UsePluginUIFeature.tsx index b3081e3b89..51188eab52 100644 --- a/src/frontend/src/hooks/UsePluginUIFeature.tsx +++ b/src/frontend/src/hooks/UsePluginUIFeature.tsx @@ -2,20 +2,24 @@ import { useQuery } from '@tanstack/react-query'; import { useMemo } from 'react'; import { api } from '../App'; -import { - PluginContext, - usePluginContext -} from '../components/plugins/PluginContext'; +import { useInvenTreeContext } from '../components/plugins/PluginContext'; import { findExternalPluginFunction } from '../components/plugins/PluginSource'; +import { + BaseUIFeature, + PluginUIFeatureAPIResponse, + PluginUIFuncWithoutInvenTreeContextType +} from '../components/plugins/PluginUIFeatureTypes'; import { ApiEndpoints } from '../enums/ApiEndpoints'; import { apiUrl } from '../states/ApiState'; import { useGlobalSettingsState } from '../states/SettingsState'; -export function usePluginUIFeature< - RequestContextT extends Record, - ResponseOptionsT extends Record, - RenderContextT extends Record ->({ featureType, context }: { featureType: string; context: RequestContextT }) { +export function usePluginUIFeature({ + featureType, + context +}: { + featureType: UIFeatureT['featureType']; + context: UIFeatureT['requestContext']; +}) { const globalSettings = useGlobalSettingsState(); const pluginUiFeaturesEnabled: boolean = useMemo( @@ -24,7 +28,9 @@ export function usePluginUIFeature< ); // API query to fetch initial information on available plugin panels - const { data: pluginData } = useQuery({ + const { data: pluginData } = useQuery< + PluginUIFeatureAPIResponse[] + >({ enabled: pluginUiFeaturesEnabled && !!featureType, queryKey: ['custom-ui-features', featureType, JSON.stringify(context)], queryFn: async () => { @@ -53,31 +59,30 @@ export function usePluginUIFeature< }); // Cache the context data which is delivered to the plugins - const pluginContext = usePluginContext(); + const inventreeContext = useInvenTreeContext(); return useMemo< { - options: ResponseOptionsT; - func: ( - ref: HTMLDivElement, - params: { renderContext: RenderContextT; pluginContext: PluginContext } - ) => void; + options: UIFeatureT['responseOptions']; + func: PluginUIFuncWithoutInvenTreeContextType; }[] >(() => { return ( - pluginData?.map((feature: any) => ({ + pluginData?.map((feature) => ({ options: feature.options, - func: async (ref: HTMLDivElement, renderContext: RenderContextT) => { + func: (async (ref, renderContext) => { const func = await findExternalPluginFunction( feature.source, 'getFeature' ); + if (!func) return; + return func(ref, { renderContext, - pluginContext + inventreeContext }); - } + }) as PluginUIFuncWithoutInvenTreeContextType })) || [] ); - }, [pluginData, pluginContext]); + }, [pluginData, inventreeContext]); } diff --git a/src/frontend/src/tables/settings/TemplateTable.tsx b/src/frontend/src/tables/settings/TemplateTable.tsx index e597809f3c..cd660b007f 100644 --- a/src/frontend/src/tables/settings/TemplateTable.tsx +++ b/src/frontend/src/tables/settings/TemplateTable.tsx @@ -14,12 +14,11 @@ import { Editor } from '../../components/editors/TemplateEditor/TemplateEditor'; import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField'; import { AttachmentLink } from '../../components/items/AttachmentLink'; import { DetailDrawer } from '../../components/nav/DetailDrawer'; -import { - TemplateEditorRenderContextType, - getPluginTemplateEditor -} from '../../components/plugins/PluginUIFeature'; +import { getPluginTemplateEditor } from '../../components/plugins/PluginUIFeature'; +import { TemplateEditorUIFeature } from '../../components/plugins/PluginUIFeatureTypes'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; +import { GetIcon } from '../../functions/icons'; import { notYetImplemented } from '../../functions/notifications'; import { useFilters } from '../../hooks/UseFilter'; import { @@ -82,17 +81,7 @@ export function TemplateDrawer({ throwError: true }); - const extraEditors = usePluginUIFeature< - { - template_type: string; - template_model: string; - }, - { - title: string; - slug: string; - }, - TemplateEditorRenderContextType - >({ + const extraEditors = usePluginUIFeature({ featureType: 'template_editor', context: { template_type: templateType, template_model: modelType } }); @@ -107,9 +96,9 @@ export function TemplateDrawer({ ...(extraEditors?.map( (editor) => ({ - key: editor.options.slug, + key: editor.options.key, name: editor.options.title, - icon: IconFileCode, + icon: GetIcon(editor.options.icon), component: getPluginTemplateEditor(editor.func, template) } as Editor) ) || [])