mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-17 12:35:46 +00:00
UI plugins custom features (#8137)
* initial implementation to let plugins provide custom ui features
* provide exportable types
* refactor ref into renderContext to make it more generic and support template preview area ui plugins
* rename 'renderContext' -> 'featureContext' as not all features may render something
* allow to specify the function name via the source file string divided by a colon
* Bump api version
* add tests
* add docs
* add docs
* debug: workflow
* debug: workflow
* fix tests
* fix tests hopefully
* apply suggestions from codereview
* trigger: ci
* Prove that coverage does not work
* Revert "Prove that coverage does not work"
This reverts commit 920c58ea6f
.
* potentially fix test???
This commit is contained in:
@ -1,13 +1,16 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 258
|
||||
INVENTREE_API_VERSION = 259
|
||||
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v259 - 2024-09-20 : https://github.com/inventree/InvenTree/pull/8137
|
||||
- Implements new API endpoint for enabling custom UI features via plugins
|
||||
|
||||
v258 - 2024-09-24 : https://github.com/inventree/InvenTree/pull/8163
|
||||
- Enhances the existing PartScheduling API endpoint
|
||||
- Adds a formal DRF serializer to the endpoint
|
||||
|
@ -11,15 +11,12 @@ 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.exceptions import log_error
|
||||
from InvenTree.filters import SEARCH_ORDER_FILTER
|
||||
from InvenTree.mixins import (
|
||||
CreateAPI,
|
||||
@ -33,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
|
||||
|
||||
@ -417,43 +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):
|
||||
try:
|
||||
# Allow plugins to fill this data out
|
||||
plugin_panels = _plugin.get_ui_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)
|
||||
except Exception:
|
||||
# Custom panels could not load
|
||||
# Log the error and continue
|
||||
log_error(f'{_plugin.slug}.get_ui_panels')
|
||||
|
||||
return Response(PluginSerializers.PluginPanelSerializer(panels, many=True).data)
|
||||
|
||||
|
||||
class PluginMetadataView(MetadataView):
|
||||
"""Metadata API endpoint for the PluginConfig model."""
|
||||
|
||||
@ -468,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'),
|
||||
|
@ -8,8 +8,7 @@ from django.urls import include, path, re_path, reverse
|
||||
|
||||
from error_report.models import Error
|
||||
|
||||
from common.models import InvenTreeSetting
|
||||
from InvenTree.unit_test import InvenTreeAPITestCase, InvenTreeTestCase
|
||||
from InvenTree.unit_test import InvenTreeTestCase
|
||||
from plugin import InvenTreePlugin
|
||||
from plugin.base.integration.PanelMixin import PanelMixin
|
||||
from plugin.helpers import MixinNotImplementedError
|
||||
@ -479,100 +478,3 @@ class PanelMixinTests(InvenTreeTestCase):
|
||||
|
||||
plugin = Wrong()
|
||||
plugin.get_custom_panels('abc', 'abc')
|
||||
|
||||
|
||||
class UserInterfaceMixinTests(InvenTreeAPITestCase):
|
||||
"""Test the UserInterfaceMixin plugin mixin class."""
|
||||
|
||||
roles = 'all'
|
||||
|
||||
fixtures = ['part', 'category', 'location', 'stock']
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Set up the test case."""
|
||||
super().setUpTestData()
|
||||
|
||||
# Ensure that the 'sampleui' plugin is installed and active
|
||||
registry.set_plugin_state('sampleui', True)
|
||||
|
||||
# Ensure that UI plugins are enabled
|
||||
InvenTreeSetting.set_setting('ENABLE_PLUGINS_INTERFACE', True, change_user=None)
|
||||
|
||||
def test_installed(self):
|
||||
"""Test that the sample UI plugin is installed and active."""
|
||||
plugin = registry.get_plugin('sampleui')
|
||||
self.assertTrue(plugin.is_active())
|
||||
|
||||
plugins = registry.with_mixin('ui')
|
||||
self.assertGreater(len(plugins), 0)
|
||||
|
||||
def test_panels(self):
|
||||
"""Test that the sample UI plugin provides custom panels."""
|
||||
from part.models import Part
|
||||
|
||||
plugin = registry.get_plugin('sampleui')
|
||||
|
||||
_part = Part.objects.first()
|
||||
|
||||
# Ensure that the part is active
|
||||
_part.active = True
|
||||
_part.save()
|
||||
|
||||
url = reverse('api-plugin-panel-list')
|
||||
|
||||
query_data = {'target_model': 'part', 'target_id': _part.pk}
|
||||
|
||||
# Enable *all* plugin settings
|
||||
plugin.set_setting('ENABLE_PART_PANELS', True)
|
||||
plugin.set_setting('ENABLE_PURCHASE_ORDER_PANELS', True)
|
||||
plugin.set_setting('ENABLE_BROKEN_PANELS', True)
|
||||
plugin.set_setting('ENABLE_DYNAMIC_PANEL', True)
|
||||
|
||||
# Request custom panel information for a part instance
|
||||
response = self.get(url, data=query_data)
|
||||
|
||||
# There should be 4 active panels for the part by default
|
||||
self.assertEqual(4, len(response.data))
|
||||
|
||||
_part.active = False
|
||||
_part.save()
|
||||
|
||||
response = self.get(url, data=query_data)
|
||||
|
||||
# As the part is not active, only 3 panels left
|
||||
self.assertEqual(3, len(response.data))
|
||||
|
||||
# Disable the "ENABLE_PART_PANELS" setting, and try again
|
||||
plugin.set_setting('ENABLE_PART_PANELS', False)
|
||||
|
||||
response = self.get(url, data=query_data)
|
||||
|
||||
# There should still be 3 panels
|
||||
self.assertEqual(3, len(response.data))
|
||||
|
||||
# Check for the correct panel names
|
||||
self.assertEqual(response.data[0]['name'], 'sample_panel')
|
||||
self.assertIn('content', response.data[0])
|
||||
self.assertNotIn('source', response.data[0])
|
||||
|
||||
self.assertEqual(response.data[1]['name'], 'broken_panel')
|
||||
self.assertEqual(response.data[1]['source'], '/this/does/not/exist.js')
|
||||
self.assertNotIn('content', response.data[1])
|
||||
|
||||
self.assertEqual(response.data[2]['name'], 'dynamic_panel')
|
||||
self.assertEqual(response.data[2]['source'], '/static/plugin/sample_panel.js')
|
||||
self.assertNotIn('content', response.data[2])
|
||||
|
||||
# Next, disable the global setting for UI integration
|
||||
InvenTreeSetting.set_setting(
|
||||
'ENABLE_PLUGINS_INTERFACE', False, change_user=None
|
||||
)
|
||||
|
||||
response = self.get(url, data=query_data)
|
||||
|
||||
# There should be no panels available
|
||||
self.assertEqual(0, len(response.data))
|
||||
|
||||
# Set the setting back to True for subsequent tests
|
||||
InvenTreeSetting.set_setting('ENABLE_PLUGINS_INTERFACE', True, change_user=None)
|
||||
|
0
src/backend/InvenTree/plugin/base/ui/__init__.py
Normal file
0
src/backend/InvenTree/plugin/base/ui/__init__.py
Normal file
94
src/backend/InvenTree/plugin/base/ui/api.py
Normal file
94
src/backend/InvenTree/plugin/base/ui/api.py
Normal file
@ -0,0 +1,94 @@
|
||||
"""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 InvenTree.exceptions import log_error
|
||||
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):
|
||||
try:
|
||||
# Allow plugins to fill this data out
|
||||
plugin_panels = _plugin.get_ui_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)
|
||||
except Exception:
|
||||
# Custom panels could not load
|
||||
# Log the error and continue
|
||||
log_error(f'{_plugin.slug}.get_ui_panels')
|
||||
|
||||
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 plugin_features and type(plugin_features) is list:
|
||||
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/<str:feature>/',
|
||||
PluginUIFeatureList.as_view(),
|
||||
name='api-plugin-ui-feature-list',
|
||||
),
|
||||
]
|
@ -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.
|
||||
|
||||
@ -48,7 +65,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_ui_panels(
|
||||
self, instance_type: str, instance_id: int, request: Request, **kwargs
|
||||
@ -78,3 +95,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 []
|
68
src/backend/InvenTree/plugin/base/ui/serializers.py
Normal file
68
src/backend/InvenTree/plugin/base/ui/serializers.py
Normal file
@ -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
|
||||
)
|
157
src/backend/InvenTree/plugin/base/ui/tests.py
Normal file
157
src/backend/InvenTree/plugin/base/ui/tests.py
Normal file
@ -0,0 +1,157 @@
|
||||
"""Unit tests for base mixins for plugins."""
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
from common.models import InvenTreeSetting
|
||||
from InvenTree.unit_test import InvenTreeAPITestCase
|
||||
from plugin.registry import registry
|
||||
|
||||
|
||||
class UserInterfaceMixinTests(InvenTreeAPITestCase):
|
||||
"""Test the UserInterfaceMixin plugin mixin class."""
|
||||
|
||||
roles = 'all'
|
||||
|
||||
fixtures = ['part', 'category', 'location', 'stock']
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Set up the test case."""
|
||||
super().setUpTestData()
|
||||
|
||||
# Ensure that the 'sampleui' plugin is installed and active
|
||||
registry.set_plugin_state('sampleui', True)
|
||||
|
||||
# Ensure that UI plugins are enabled
|
||||
InvenTreeSetting.set_setting('ENABLE_PLUGINS_INTERFACE', True, change_user=None)
|
||||
|
||||
def test_installed(self):
|
||||
"""Test that the sample UI plugin is installed and active."""
|
||||
plugin = registry.get_plugin('sampleui')
|
||||
self.assertTrue(plugin.is_active())
|
||||
|
||||
plugins = registry.with_mixin('ui')
|
||||
self.assertGreater(len(plugins), 0)
|
||||
|
||||
def test_panels(self):
|
||||
"""Test that the sample UI plugin provides custom panels."""
|
||||
from part.models import Part
|
||||
|
||||
plugin = registry.get_plugin('sampleui')
|
||||
|
||||
_part = Part.objects.first()
|
||||
|
||||
# Ensure that the part is active
|
||||
_part.active = True
|
||||
_part.save()
|
||||
|
||||
url = reverse('api-plugin-panel-list')
|
||||
|
||||
query_data = {'target_model': 'part', 'target_id': _part.pk}
|
||||
|
||||
# Enable *all* plugin settings
|
||||
plugin.set_setting('ENABLE_PART_PANELS', True)
|
||||
plugin.set_setting('ENABLE_PURCHASE_ORDER_PANELS', True)
|
||||
plugin.set_setting('ENABLE_BROKEN_PANELS', True)
|
||||
plugin.set_setting('ENABLE_DYNAMIC_PANEL', True)
|
||||
|
||||
# Request custom panel information for a part instance
|
||||
response = self.get(url, data=query_data)
|
||||
|
||||
# There should be 4 active panels for the part by default
|
||||
self.assertEqual(4, len(response.data))
|
||||
|
||||
_part.active = False
|
||||
_part.save()
|
||||
|
||||
response = self.get(url, data=query_data)
|
||||
|
||||
# As the part is not active, only 3 panels left
|
||||
self.assertEqual(3, len(response.data))
|
||||
|
||||
# Disable the "ENABLE_PART_PANELS" setting, and try again
|
||||
plugin.set_setting('ENABLE_PART_PANELS', False)
|
||||
|
||||
response = self.get(url, data=query_data)
|
||||
|
||||
# There should still be 3 panels
|
||||
self.assertEqual(3, len(response.data))
|
||||
|
||||
# Check for the correct panel names
|
||||
self.assertEqual(response.data[0]['name'], 'sample_panel')
|
||||
self.assertIn('content', response.data[0])
|
||||
self.assertNotIn('source', response.data[0])
|
||||
|
||||
self.assertEqual(response.data[1]['name'], 'broken_panel')
|
||||
self.assertEqual(response.data[1]['source'], '/this/does/not/exist.js')
|
||||
self.assertNotIn('content', response.data[1])
|
||||
|
||||
self.assertEqual(response.data[2]['name'], 'dynamic_panel')
|
||||
self.assertEqual(response.data[2]['source'], '/static/plugin/sample_panel.js')
|
||||
self.assertNotIn('content', response.data[2])
|
||||
|
||||
# Next, disable the global setting for UI integration
|
||||
InvenTreeSetting.set_setting(
|
||||
'ENABLE_PLUGINS_INTERFACE', False, change_user=None
|
||||
)
|
||||
|
||||
response = self.get(url, data=query_data)
|
||||
|
||||
# There should be no panels available
|
||||
self.assertEqual(0, len(response.data))
|
||||
|
||||
# Set the setting back to True for subsequent tests
|
||||
InvenTreeSetting.set_setting('ENABLE_PLUGINS_INTERFACE', True, change_user=None)
|
||||
|
||||
def test_ui_features(self):
|
||||
"""Test that the sample UI plugin provides custom features."""
|
||||
template_editor_url = reverse(
|
||||
'api-plugin-ui-feature-list', kwargs={'feature': 'template_editor'}
|
||||
)
|
||||
template_preview_url = reverse(
|
||||
'api-plugin-ui-feature-list', kwargs={'feature': 'template_preview'}
|
||||
)
|
||||
|
||||
query_data_label = {'template_type': 'labeltemplate', 'template_model': 'part'}
|
||||
query_data_report = {
|
||||
'template_type': 'reporttemplate',
|
||||
'template_model': 'part',
|
||||
}
|
||||
|
||||
# Request custom template editor information
|
||||
response = self.get(template_editor_url, data=query_data_label)
|
||||
self.assertEqual(1, len(response.data))
|
||||
|
||||
response = self.get(template_editor_url, data=query_data_report)
|
||||
self.assertEqual(0, len(response.data))
|
||||
|
||||
response = self.get(template_preview_url, data=query_data_report)
|
||||
self.assertEqual(1, len(response.data))
|
||||
|
||||
# Check for the correct feature details here
|
||||
self.assertEqual(response.data[0]['feature_type'], 'template_preview')
|
||||
self.assertDictEqual(
|
||||
response.data[0]['options'],
|
||||
{
|
||||
'key': 'sample-template-preview',
|
||||
'title': 'Sample Template Preview',
|
||||
'icon': 'category',
|
||||
},
|
||||
)
|
||||
self.assertEqual(
|
||||
response.data[0]['source'],
|
||||
'/static/plugin/sample_template.js:getTemplatePreview',
|
||||
)
|
||||
|
||||
# Next, disable the global setting for UI integration
|
||||
InvenTreeSetting.set_setting(
|
||||
'ENABLE_PLUGINS_INTERFACE', False, change_user=None
|
||||
)
|
||||
|
||||
response = self.get(template_editor_url, data=query_data_label)
|
||||
|
||||
# There should be no features available
|
||||
self.assertEqual(0, len(response.data))
|
||||
|
||||
# Set the setting back to True for subsequent tests
|
||||
InvenTreeSetting.set_setting('ENABLE_PLUGINS_INTERFACE', True, change_user=None)
|
@ -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',
|
||||
|
@ -122,3 +122,36 @@ class SampleUserInterfacePlugin(SettingsMixin, UserInterfaceMixin, InvenTreePlug
|
||||
})
|
||||
|
||||
return panels
|
||||
|
||||
def get_ui_features(self, feature_type, context, request):
|
||||
"""Return a list of custom features to be injected into the UI."""
|
||||
if (
|
||||
feature_type == 'template_editor'
|
||||
and context.get('template_type') == 'labeltemplate'
|
||||
):
|
||||
return [
|
||||
{
|
||||
'feature_type': 'template_editor',
|
||||
'options': {
|
||||
'key': 'sample-template-editor',
|
||||
'title': 'Sample Template Editor',
|
||||
'icon': 'keywords',
|
||||
},
|
||||
'source': '/static/plugin/sample_template.js:getTemplateEditor',
|
||||
}
|
||||
]
|
||||
|
||||
if feature_type == 'template_preview':
|
||||
return [
|
||||
{
|
||||
'feature_type': 'template_preview',
|
||||
'options': {
|
||||
'key': 'sample-template-preview',
|
||||
'title': 'Sample Template Preview',
|
||||
'icon': 'category',
|
||||
},
|
||||
'source': '/static/plugin/sample_template.js:getTemplatePreview',
|
||||
}
|
||||
]
|
||||
|
||||
return []
|
||||
|
@ -0,0 +1,33 @@
|
||||
export function getTemplateEditor({ featureContext, pluginContext }) {
|
||||
const { ref } = featureContext;
|
||||
console.log("Template editor feature was called with", featureContext, pluginContext);
|
||||
const t = document.createElement("textarea");
|
||||
t.id = 'sample-template-editor-textarea';
|
||||
t.rows = 25;
|
||||
t.cols = 60;
|
||||
|
||||
featureContext.registerHandlers({
|
||||
setCode: (code) => {
|
||||
t.value = code;
|
||||
},
|
||||
getCode: () => {
|
||||
return t.value;
|
||||
}
|
||||
});
|
||||
|
||||
ref.innerHTML = "";
|
||||
ref.appendChild(t);
|
||||
}
|
||||
|
||||
export function getTemplatePreview({ featureContext, pluginContext }) {
|
||||
const { ref } = featureContext;
|
||||
console.log("Template preview feature was called with", featureContext, pluginContext);
|
||||
|
||||
featureContext.registerHandlers({
|
||||
updatePreview: (...args) => {
|
||||
console.log("updatePreview", args);
|
||||
}
|
||||
});
|
||||
|
||||
ref.innerHTML = "<h1>Hello world</h1>";
|
||||
}
|
@ -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
|
||||
)
|
||||
|
Reference in New Issue
Block a user