From e2354640656c31a9fc51961c31c5b7a0a7c2eaa3 Mon Sep 17 00:00:00 2001 From: wolflu05 <76838159+wolflu05@users.noreply.github.com> Date: Tue, 17 Sep 2024 20:36:25 +0200 Subject: [PATCH] initial implementation to let plugins provide custom ui features --- docs/docs/extend/plugins/ui.md | 2 +- src/backend/InvenTree/plugin/api.py | 52 +--------- .../InvenTree/plugin/base/ui/__init__.py | 0 src/backend/InvenTree/plugin/base/ui/api.py | 90 +++++++++++++++++ .../UserInterfaceMixin.py => ui/mixins.py} | 37 ++++++- .../InvenTree/plugin/base/ui/serializers.py | 68 +++++++++++++ .../InvenTree/plugin/mixins/__init__.py | 2 +- src/backend/InvenTree/plugin/serializers.py | 43 -------- .../src/components/plugins/PluginContext.tsx | 54 +++++++++- .../components/plugins/PluginUIFeature.tsx | 99 +++++++++++++++++++ src/frontend/src/enums/ApiEndpoints.tsx | 1 + src/frontend/src/hooks/UsePluginPanels.tsx | 44 ++------- src/frontend/src/hooks/UsePluginUIFeature.tsx | 83 ++++++++++++++++ .../AdminCenter/LabelTemplatePanel.tsx | 1 + .../AdminCenter/ReportTemplatePanel.tsx | 1 + .../src/tables/settings/TemplateTable.tsx | 48 ++++++++- 16 files changed, 487 insertions(+), 138 deletions(-) create mode 100644 src/backend/InvenTree/plugin/base/ui/__init__.py create mode 100644 src/backend/InvenTree/plugin/base/ui/api.py rename src/backend/InvenTree/plugin/base/{integration/UserInterfaceMixin.py => ui/mixins.py} (67%) create mode 100644 src/backend/InvenTree/plugin/base/ui/serializers.py create mode 100644 src/frontend/src/components/plugins/PluginUIFeature.tsx create mode 100644 src/frontend/src/hooks/UsePluginUIFeature.tsx diff --git a/docs/docs/extend/plugins/ui.md b/docs/docs/extend/plugins/ui.md index 33b58faabe..ae350a209d 100644 --- a/docs/docs/extend/plugins/ui.md +++ b/docs/docs/extend/plugins/ui.md @@ -20,7 +20,7 @@ When rendering certain content in the user interface, the rendering functions ar Many of the pages in the InvenTree web interface are built using a series of "panels" which are displayed on the page. Custom panels can be added to these pages, by implementing the `get_custom_panels` method: -::: plugin.base.integration.UserInterfaceMixin.UserInterfaceMixin.get_custom_panels +::: plugin.base.ui.mixins.UserInterfaceMixin.get_custom_panels options: show_bases: False show_root_heading: False diff --git a/src/backend/InvenTree/plugin/api.py b/src/backend/InvenTree/plugin/api.py index a8a9cb1c48..ff1e301357 100644 --- a/src/backend/InvenTree/plugin/api.py +++ b/src/backend/InvenTree/plugin/api.py @@ -11,13 +11,11 @@ from django_filters.rest_framework import DjangoFilterBackend from drf_spectacular.utils import extend_schema from rest_framework import permissions, status from rest_framework.exceptions import NotFound -from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView import plugin.serializers as PluginSerializers from common.api import GlobalSettingsPermissions -from common.settings import get_global_setting from InvenTree.api import MetadataView from InvenTree.filters import SEARCH_ORDER_FILTER from InvenTree.mixins import ( @@ -32,6 +30,7 @@ from plugin import registry from plugin.base.action.api import ActionPluginView from plugin.base.barcodes.api import barcode_api_urls from plugin.base.locate.api import LocatePluginView +from plugin.base.ui.api import ui_plugins_api_urls from plugin.models import PluginConfig, PluginSetting from plugin.plugin import InvenTreePlugin @@ -416,38 +415,6 @@ class RegistryStatusView(APIView): return Response(result) -class PluginPanelList(APIView): - """API endpoint for listing all available plugin panels.""" - - permission_classes = [IsAuthenticated] - serializer_class = PluginSerializers.PluginPanelSerializer - - @extend_schema(responses={200: PluginSerializers.PluginPanelSerializer(many=True)}) - def get(self, request): - """Show available plugin panels.""" - target_model = request.query_params.get('target_model', None) - target_id = request.query_params.get('target_id', None) - - panels = [] - - if get_global_setting('ENABLE_PLUGINS_INTERFACE'): - # Extract all plugins from the registry which provide custom panels - for _plugin in registry.with_mixin('ui', active=True): - # Allow plugins to fill this data out - plugin_panels = _plugin.get_custom_panels( - target_model, target_id, request - ) - - if plugin_panels and type(plugin_panels) is list: - for panel in plugin_panels: - panel['plugin'] = _plugin.slug - - # TODO: Validate each panel before inserting - panels.append(panel) - - return Response(PluginSerializers.PluginPanelSerializer(panels, many=True).data) - - class PluginMetadataView(MetadataView): """Metadata API endpoint for the PluginConfig model.""" @@ -462,21 +429,8 @@ plugin_api_urls = [ path( 'plugins/', include([ - path( - 'ui/', - include([ - path( - 'panels/', - include([ - path( - '', - PluginPanelList.as_view(), - name='api-plugin-panel-list', - ) - ]), - ) - ]), - ), + # UI plugins + path('ui/', include(ui_plugins_api_urls)), # Plugin management path('reload/', PluginReload.as_view(), name='api-plugin-reload'), path('install/', PluginInstall.as_view(), name='api-plugin-install'), diff --git a/src/backend/InvenTree/plugin/base/ui/__init__.py b/src/backend/InvenTree/plugin/base/ui/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/backend/InvenTree/plugin/base/ui/api.py b/src/backend/InvenTree/plugin/base/ui/api.py new file mode 100644 index 0000000000..077b3971ce --- /dev/null +++ b/src/backend/InvenTree/plugin/base/ui/api.py @@ -0,0 +1,90 @@ +"""API for UI plugins.""" + +from django.urls import path + +from drf_spectacular.utils import extend_schema +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +import plugin.base.ui.serializers as UIPluginSerializers +from common.settings import get_global_setting +from plugin import registry + + +class PluginPanelList(APIView): + """API endpoint for listing all available plugin panels.""" + + permission_classes = [IsAuthenticated] + serializer_class = UIPluginSerializers.PluginPanelSerializer + + @extend_schema( + responses={200: UIPluginSerializers.PluginPanelSerializer(many=True)} + ) + def get(self, request): + """Show available plugin panels.""" + target_model = request.query_params.get('target_model', None) + target_id = request.query_params.get('target_id', None) + + panels = [] + + if get_global_setting('ENABLE_PLUGINS_INTERFACE'): + # Extract all plugins from the registry which provide custom panels + for _plugin in registry.with_mixin('ui', active=True): + # Allow plugins to fill this data out + plugin_panels = _plugin.get_custom_panels( + target_model, target_id, request + ) + + if plugin_panels and type(plugin_panels) is list: + for panel in plugin_panels: + panel['plugin'] = _plugin.slug + + # TODO: Validate each panel before inserting + panels.append(panel) + + return Response( + UIPluginSerializers.PluginPanelSerializer(panels, many=True).data + ) + + +class PluginUIFeatureList(APIView): + """API endpoint for listing all available plugin ui features.""" + + permission_classes = [IsAuthenticated] + serializer_class = UIPluginSerializers.PluginUIFeatureSerializer + + @extend_schema( + responses={200: UIPluginSerializers.PluginUIFeatureSerializer(many=True)} + ) + def get(self, request, feature): + """Show available plugin ui features.""" + features = [] + + if get_global_setting('ENABLE_PLUGINS_INTERFACE'): + # Extract all plugins from the registry which provide custom ui features + for _plugin in registry.with_mixin('ui', active=True): + # Allow plugins to fill this data out + plugin_features = _plugin.get_ui_features( + feature, request.query_params, request + ) + + if not plugin_features or type(plugin_features) is not list: + continue + + for feature in plugin_features: + features.append(feature) + + return Response( + UIPluginSerializers.PluginUIFeatureSerializer(features, many=True).data + ) + + +ui_plugins_api_urls = [ + path('panels/', PluginPanelList.as_view(), name='api-plugin-panel-list'), + path( + 'features//', + PluginUIFeatureList.as_view(), + name='api-plugin-ui-feature-list', + ), +] diff --git a/src/backend/InvenTree/plugin/base/integration/UserInterfaceMixin.py b/src/backend/InvenTree/plugin/base/ui/mixins.py similarity index 67% rename from src/backend/InvenTree/plugin/base/integration/UserInterfaceMixin.py rename to src/backend/InvenTree/plugin/base/ui/mixins.py index 252c18705b..47e582c596 100644 --- a/src/backend/InvenTree/plugin/base/integration/UserInterfaceMixin.py +++ b/src/backend/InvenTree/plugin/base/ui/mixins.py @@ -4,7 +4,7 @@ Allows integration of custom UI elements into the React user interface. """ import logging -from typing import TypedDict +from typing import Literal, TypedDict from rest_framework.request import Request @@ -29,6 +29,23 @@ class CustomPanel(TypedDict): source: str +FeatureType = Literal['template_editor', 'template_preview'] + + +class UIFeature(TypedDict): + """Base type definition for a ui feature. + + Attributes: + feature_type: The feature type (required, see documentation for all available types) + options: Feature options (required, see documentation for all available options for each type) + source: The source of the feature (required, path to a JavaScript file). + """ + + feature_type: FeatureType + options: dict + source: str + + class UserInterfaceMixin: """Plugin mixin class which handles injection of custom elements into the front-end interface. @@ -44,7 +61,7 @@ class UserInterfaceMixin: def __init__(self): """Register mixin.""" super().__init__() - self.add_mixin('ui', True, __class__) + self.add_mixin('ui', True, __class__) # type: ignore def get_custom_panels( self, instance_type: str, instance_id: int, request: Request @@ -74,3 +91,19 @@ class UserInterfaceMixin: """ # Default implementation returns an empty list return [] + + def get_ui_features( + self, feature_type: FeatureType, context: dict, request: Request + ) -> list[UIFeature]: + """Return a list of custom features to be injected into the UI. + + Arguments: + feature_type: The type of feature being requested + context: Additional context data provided by the UI + request: HTTPRequest object (including user information) + + Returns: + list: A list of custom UIFeature dicts to be injected into the UI + """ + # Default implementation returns an empty list + return [] diff --git a/src/backend/InvenTree/plugin/base/ui/serializers.py b/src/backend/InvenTree/plugin/base/ui/serializers.py new file mode 100644 index 0000000000..18c69bb1af --- /dev/null +++ b/src/backend/InvenTree/plugin/base/ui/serializers.py @@ -0,0 +1,68 @@ +"""Serializers for UI plugin api.""" + +from django.utils.translation import gettext_lazy as _ + +from rest_framework import serializers + + +class PluginPanelSerializer(serializers.Serializer): + """Serializer for a plugin panel.""" + + class Meta: + """Meta for serializer.""" + + fields = [ + 'plugin', + 'name', + 'label', + # Following fields are optional + 'icon', + 'content', + 'source', + ] + + # Required fields + plugin = serializers.CharField( + label=_('Plugin Key'), required=True, allow_blank=False + ) + + name = serializers.CharField( + label=_('Panel Name'), required=True, allow_blank=False + ) + + label = serializers.CharField( + label=_('Panel Title'), required=True, allow_blank=False + ) + + # Optional fields + icon = serializers.CharField( + label=_('Panel Icon'), required=False, allow_blank=True + ) + + content = serializers.CharField( + label=_('Panel Content (HTML)'), required=False, allow_blank=True + ) + + source = serializers.CharField( + label=_('Panel Source (javascript)'), required=False, allow_blank=True + ) + + +class PluginUIFeatureSerializer(serializers.Serializer): + """Serializer for a plugin ui feature.""" + + class Meta: + """Meta for serializer.""" + + fields = ['feature_type', 'options', 'source'] + + # Required fields + feature_type = serializers.CharField( + label=_('Feature Type'), required=True, allow_blank=False + ) + + options = serializers.DictField(label=_('Feature Options'), required=True) + + source = serializers.CharField( + label=_('Feature Source (javascript)'), required=True, allow_blank=False + ) diff --git a/src/backend/InvenTree/plugin/mixins/__init__.py b/src/backend/InvenTree/plugin/mixins/__init__.py index 1de17d613d..bd5851f667 100644 --- a/src/backend/InvenTree/plugin/mixins/__init__.py +++ b/src/backend/InvenTree/plugin/mixins/__init__.py @@ -14,10 +14,10 @@ from plugin.base.integration.ReportMixin import ReportMixin from plugin.base.integration.ScheduleMixin import ScheduleMixin from plugin.base.integration.SettingsMixin import SettingsMixin from plugin.base.integration.UrlsMixin import UrlsMixin -from plugin.base.integration.UserInterfaceMixin import UserInterfaceMixin from plugin.base.integration.ValidationMixin import ValidationMixin from plugin.base.label.mixins import LabelPrintingMixin from plugin.base.locate.mixins import LocateMixin +from plugin.base.ui.mixins import UserInterfaceMixin __all__ = [ 'APICallMixin', diff --git a/src/backend/InvenTree/plugin/serializers.py b/src/backend/InvenTree/plugin/serializers.py index 787c9ad2b4..eec4cd0420 100644 --- a/src/backend/InvenTree/plugin/serializers.py +++ b/src/backend/InvenTree/plugin/serializers.py @@ -301,46 +301,3 @@ class PluginRelationSerializer(serializers.PrimaryKeyRelatedField): def to_representation(self, value): """Return the 'key' of the PluginConfig object.""" return value.key - - -class PluginPanelSerializer(serializers.Serializer): - """Serializer for a plugin panel.""" - - class Meta: - """Meta for serializer.""" - - fields = [ - 'plugin', - 'name', - 'label', - # Following fields are optional - 'icon', - 'content', - 'source', - ] - - # Required fields - plugin = serializers.CharField( - label=_('Plugin Key'), required=True, allow_blank=False - ) - - name = serializers.CharField( - label=_('Panel Name'), required=True, allow_blank=False - ) - - label = serializers.CharField( - label=_('Panel Title'), required=True, allow_blank=False - ) - - # Optional fields - icon = serializers.CharField( - label=_('Panel Icon'), required=False, allow_blank=True - ) - - content = serializers.CharField( - label=_('Panel Content (HTML)'), required=False, allow_blank=True - ) - - source = serializers.CharField( - label=_('Panel Source (javascript)'), required=False, allow_blank=True - ) diff --git a/src/frontend/src/components/plugins/PluginContext.tsx b/src/frontend/src/components/plugins/PluginContext.tsx index a2ed19c937..9a134168d8 100644 --- a/src/frontend/src/components/plugins/PluginContext.tsx +++ b/src/frontend/src/components/plugins/PluginContext.tsx @@ -1,10 +1,22 @@ -import { MantineColorScheme, MantineTheme } from '@mantine/core'; +import { + MantineColorScheme, + MantineTheme, + useMantineColorScheme, + useMantineTheme +} from '@mantine/core'; import { AxiosInstance } from 'axios'; -import { NavigateFunction } from 'react-router-dom'; +import { useMemo } from 'react'; +import { NavigateFunction, useNavigate } from 'react-router-dom'; +import { api } from '../../App'; import { ModelType } from '../../enums/ModelType'; -import { SettingsStateProps } from '../../states/SettingsState'; -import { UserStateProps } from '../../states/UserState'; +import { useLocalState } from '../../states/LocalState'; +import { + SettingsStateProps, + useGlobalSettingsState, + useUserSettingsState +} from '../../states/SettingsState'; +import { UserStateProps, useUserState } from '../../states/UserState'; /* * A set of properties which are passed to a plugin, @@ -34,3 +46,37 @@ export type PluginContext = { theme: MantineTheme; colorScheme: MantineColorScheme; }; + +export const usePluginContext = () => { + const host = useLocalState.getState().host; + const navigate = useNavigate(); + const user = useUserState(); + const { colorScheme } = useMantineColorScheme(); + const theme = useMantineTheme(); + const globalSettings = useGlobalSettingsState(); + const userSettings = useUserSettingsState(); + + const contextData: PluginContext = useMemo(() => { + return { + user: user, + host: host, + api: api, + navigate: navigate, + globalSettings: globalSettings, + userSettings: userSettings, + theme: theme, + colorScheme: colorScheme + }; + }, [ + user, + host, + api, + navigate, + globalSettings, + userSettings, + theme, + colorScheme + ]); + + return contextData; +}; diff --git a/src/frontend/src/components/plugins/PluginUIFeature.tsx b/src/frontend/src/components/plugins/PluginUIFeature.tsx new file mode 100644 index 0000000000..d074f1a8a9 --- /dev/null +++ b/src/frontend/src/components/plugins/PluginUIFeature.tsx @@ -0,0 +1,99 @@ +import { t } from '@lingui/macro'; +import { Alert, Stack, Text } from '@mantine/core'; +import { IconExclamationCircle } from '@tabler/icons-react'; +import { + forwardRef, + useEffect, + useImperativeHandle, + useRef, + useState +} from 'react'; + +import { TemplateI } from '../../tables/settings/TemplateTable'; +import { EditorComponent } from '../editors/TemplateEditor/TemplateEditor'; + +// 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) => + forwardRef((props, ref) => { + const elRef = useRef(); + const [error, setError] = useState(undefined); + + const initialCodeRef = useRef(); + const setCodeRef = useRef<(code: string) => void>(); + const getCodeRef = useRef<() => string>(); + + useImperativeHandle(ref, () => ({ + setCode: (code) => { + // if the editor is not yet initialized, store the initial code in a ref to set it later + if (setCodeRef.current) { + setCodeRef.current(code); + } else { + initialCodeRef.current = code; + } + }, + getCode: () => getCodeRef.current?.() + })); + + useEffect(() => { + (async () => { + try { + await func(elRef.current, { + registerHandlers: ({ getCode, setCode }) => { + setCodeRef.current = setCode; + getCodeRef.current = getCode; + + if (initialCodeRef.current) { + setCode(initialCodeRef.current); + } + }, + template + } as TemplateEditorRenderContextType); + } catch (error) { + setError(t`Error occurred while rendering the template editor.`); + console.error(error); + } + })(); + }, []); + + return ( + + {error && ( + } + > + {error} + + )} +
+
+ ); + }) as EditorComponent; diff --git a/src/frontend/src/enums/ApiEndpoints.tsx b/src/frontend/src/enums/ApiEndpoints.tsx index d99cd90ef1..b5aa8e2da1 100644 --- a/src/frontend/src/enums/ApiEndpoints.tsx +++ b/src/frontend/src/enums/ApiEndpoints.tsx @@ -184,6 +184,7 @@ export enum ApiEndpoints { // User interface plugin endpoints plugin_panel_list = 'plugins/ui/panels/', + plugin_ui_features_list = 'plugins/ui/features/:feature_type/', // Machine API endpoints machine_types_list = 'machine/types/', diff --git a/src/frontend/src/hooks/UsePluginPanels.tsx b/src/frontend/src/hooks/UsePluginPanels.tsx index aa676278f4..ed9abb1734 100644 --- a/src/frontend/src/hooks/UsePluginPanels.tsx +++ b/src/frontend/src/hooks/UsePluginPanels.tsx @@ -1,11 +1,12 @@ -import { useMantineColorScheme, useMantineTheme } from '@mantine/core'; import { useQuery } from '@tanstack/react-query'; import { useEffect, useMemo, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; import { api } from '../App'; import { PanelType } from '../components/nav/Panel'; -import { PluginContext } from '../components/plugins/PluginContext'; +import { + PluginContext, + usePluginContext +} from '../components/plugins/PluginContext'; import PluginPanelContent, { PluginPanelProps, isPluginPanelHidden @@ -15,12 +16,7 @@ import { ModelType } from '../enums/ModelType'; import { identifierString } from '../functions/conversion'; import { InvenTreeIcon, InvenTreeIconType } from '../functions/icons'; import { apiUrl } from '../states/ApiState'; -import { useLocalState } from '../states/LocalState'; -import { - useGlobalSettingsState, - useUserSettingsState -} from '../states/SettingsState'; -import { useUserState } from '../states/UserState'; +import { useGlobalSettingsState } from '../states/SettingsState'; export function usePluginPanels({ instance, @@ -31,13 +27,7 @@ export function usePluginPanels({ model?: ModelType | string; id?: string | number | null; }): PanelType[] { - const host = useLocalState.getState().host; - const navigate = useNavigate(); - const user = useUserState(); - const { colorScheme } = useMantineColorScheme(); - const theme = useMantineTheme(); const globalSettings = useGlobalSettingsState(); - const userSettings = useUserSettingsState(); const pluginPanelsEnabled: boolean = useMemo( () => globalSettings.isSet('ENABLE_PLUGINS_INTERFACE'), @@ -69,33 +59,15 @@ export function usePluginPanels({ }); // Cache the context data which is delivered to the plugins + const pluginContext = usePluginContext(); const contextData: PluginContext = useMemo(() => { return { model: model, id: id, instance: instance, - user: user, - host: host, - api: api, - navigate: navigate, - globalSettings: globalSettings, - userSettings: userSettings, - theme: theme, - colorScheme: colorScheme + ...pluginContext }; - }, [ - model, - id, - instance, - user, - host, - api, - navigate, - globalSettings, - userSettings, - theme, - colorScheme - ]); + }, [model, id, instance]); // Track which panels are hidden: { panelName: true/false } // We need to memoize this as the plugins can determine this dynamically diff --git a/src/frontend/src/hooks/UsePluginUIFeature.tsx b/src/frontend/src/hooks/UsePluginUIFeature.tsx new file mode 100644 index 0000000000..b3081e3b89 --- /dev/null +++ b/src/frontend/src/hooks/UsePluginUIFeature.tsx @@ -0,0 +1,83 @@ +import { useQuery } from '@tanstack/react-query'; +import { useMemo } from 'react'; + +import { api } from '../App'; +import { + PluginContext, + usePluginContext +} from '../components/plugins/PluginContext'; +import { findExternalPluginFunction } from '../components/plugins/PluginSource'; +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 }) { + const globalSettings = useGlobalSettingsState(); + + const pluginUiFeaturesEnabled: boolean = useMemo( + () => globalSettings.isSet('ENABLE_PLUGINS_INTERFACE'), + [globalSettings] + ); + + // API query to fetch initial information on available plugin panels + const { data: pluginData } = useQuery({ + enabled: pluginUiFeaturesEnabled && !!featureType, + queryKey: ['custom-ui-features', featureType, JSON.stringify(context)], + queryFn: async () => { + if (!pluginUiFeaturesEnabled || !featureType) { + return Promise.resolve([]); + } + + return api + .get( + apiUrl(ApiEndpoints.plugin_ui_features_list, undefined, { + feature_type: featureType + }), + { + params: context + } + ) + .then((response: any) => response.data) + .catch((error: any) => { + console.error( + `Failed to fetch plugin ui features for feature "${featureType}":`, + error + ); + return []; + }); + } + }); + + // Cache the context data which is delivered to the plugins + const pluginContext = usePluginContext(); + + return useMemo< + { + options: ResponseOptionsT; + func: ( + ref: HTMLDivElement, + params: { renderContext: RenderContextT; pluginContext: PluginContext } + ) => void; + }[] + >(() => { + return ( + pluginData?.map((feature: any) => ({ + options: feature.options, + func: async (ref: HTMLDivElement, renderContext: RenderContextT) => { + const func = await findExternalPluginFunction( + feature.source, + 'getFeature' + ); + return func(ref, { + renderContext, + pluginContext + }); + } + })) || [] + ); + }, [pluginData, pluginContext]); +} diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/LabelTemplatePanel.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/LabelTemplatePanel.tsx index cda9c40efa..0ae54d5cbd 100644 --- a/src/frontend/src/pages/Index/Settings/AdminCenter/LabelTemplatePanel.tsx +++ b/src/frontend/src/pages/Index/Settings/AdminCenter/LabelTemplatePanel.tsx @@ -6,6 +6,7 @@ export default function LabelTemplatePanel() { return ( ({ + featureType: 'template_editor', + context: { template_type: templateType, template_model: modelType } + }); + const editors = useMemo(() => { + const editors = [CodeEditor]; + + if (!template) { + return editors; + } + + editors.push( + ...(extraEditors?.map( + (editor) => + ({ + key: editor.options.slug, + name: editor.options.title, + icon: IconFileCode, + component: getPluginTemplateEditor(editor.func, template) + } as Editor) + ) || []) + ); + + return editors; + }, [extraEditors, template]); + if (isFetching) { return ; } @@ -100,7 +144,7 @@ export function TemplateDrawer({ templateUrl={apiUrl(templateEndpoint, id)} printingUrl={apiUrl(printingEndpoint)} template={template} - editors={[CodeEditor]} + editors={editors} previewAreas={[PdfPreview]} />