diff --git a/src/backend/InvenTree/plugin/api.py b/src/backend/InvenTree/plugin/api.py
index 79840c3f23..b898eb2c98 100644
--- a/src/backend/InvenTree/plugin/api.py
+++ b/src/backend/InvenTree/plugin/api.py
@@ -424,8 +424,33 @@ class PluginPanelList(APIView):
# Extract all plugins from the registry which provide custom panels
for _plugin in registry.with_mixin('panel', active=True):
+ # TODO: Allow plugins to fill this data out
...
+ panels = [
+ {
+ 'plugin': 'myplugin',
+ 'name': 'test-plugin',
+ 'label': 'My Plugin',
+ 'icon': 'part',
+ 'content': '
hello world
',
+ },
+ {
+ 'plugin': 'myplugin',
+ 'name': 'test-plugin-2',
+ 'label': 'My Plugin 2',
+ 'icon': 'email',
+ 'content': 'hello world 2
',
+ },
+ {
+ 'plugin': 'myplugin',
+ 'name': 'test-plugin-3',
+ 'label': 'My Plugin 3',
+ 'icon': 'website',
+ 'content': 'hello world 3
',
+ },
+ ]
+
return Response(PluginSerializers.PluginPanelSerializer(panels, many=True).data)
@@ -437,7 +462,7 @@ plugin_api_urls = [
'plugins/',
include([
path(
- 'panel/',
+ 'panels/',
include([
path('', PluginPanelList.as_view(), name='api-plugin-panel-list')
]),
diff --git a/src/backend/InvenTree/plugin/serializers.py b/src/backend/InvenTree/plugin/serializers.py
index 968e06728d..8bd385c5e0 100644
--- a/src/backend/InvenTree/plugin/serializers.py
+++ b/src/backend/InvenTree/plugin/serializers.py
@@ -311,9 +311,10 @@ class PluginPanelSerializer(serializers.Serializer):
class Meta:
"""Meta for serializer."""
- fields = ['plugin', 'title', 'description', 'icon']
+ fields = ['plugin', 'name', 'label', 'icon']
plugin = serializers.CharField(label=_('Plugin Key'))
- title = serializers.CharField(label=_('Panel Title'))
- description = serializers.CharField(label=_('Panel Description'))
+ name = serializers.CharField(label=_('Panel Name'))
+ label = serializers.CharField(label=_('Panel Label'))
icon = serializers.CharField(label=_('Panel Icon'))
+ content = serializers.CharField(label=_('Panel Content'))
diff --git a/src/frontend/src/enums/ApiEndpoints.tsx b/src/frontend/src/enums/ApiEndpoints.tsx
index 962da26c49..0ce284415b 100644
--- a/src/frontend/src/enums/ApiEndpoints.tsx
+++ b/src/frontend/src/enums/ApiEndpoints.tsx
@@ -144,6 +144,7 @@ export enum ApiEndpoints {
plugin_reload = 'plugins/reload/',
plugin_activate = 'plugins/:key/activate/',
plugin_uninstall = 'plugins/:key/uninstall/',
+ plugin_panel_list = 'plugins/panels/',
// Machine API endpoints
machine_types_list = 'machine/types/',
diff --git a/src/frontend/src/functions/icons.tsx b/src/frontend/src/functions/icons.tsx
index 75002f4701..a572b99e3e 100644
--- a/src/frontend/src/functions/icons.tsx
+++ b/src/frontend/src/functions/icons.tsx
@@ -50,6 +50,7 @@ import {
IconPaperclip,
IconPhone,
IconPhoto,
+ IconPlug,
IconPoint,
IconPrinter,
IconProgressCheck,
@@ -199,7 +200,8 @@ const icons = {
destination: IconFlag,
repeat_destination: IconFlagShare,
unlink: IconUnlink,
- success: IconCircleCheck
+ success: IconCircleCheck,
+ plugin: IconPlug
};
export type InvenTreeIconType = keyof typeof icons;
diff --git a/src/frontend/src/hooks/UsePluginPanels.tsx b/src/frontend/src/hooks/UsePluginPanels.tsx
index 3f730c4d43..e172c3bb36 100644
--- a/src/frontend/src/hooks/UsePluginPanels.tsx
+++ b/src/frontend/src/hooks/UsePluginPanels.tsx
@@ -1,58 +1,70 @@
+import { t } from '@lingui/macro';
+import { Alert, Text } from '@mantine/core';
import { useTimeout } from '@mantine/hooks';
import { Icon24Hours } from '@tabler/icons-react';
+import { useQuery } from '@tanstack/react-query';
import { ReactNode, useEffect, useMemo, useState } from 'react';
+import { api } from '../App';
import { PanelType } from '../components/nav/Panel';
+import { ApiEndpoints } from '../enums/ApiEndpoints';
+import { identifierString } from '../functions/conversion';
+import { InvenTreeIcon } from '../functions/icons';
+import { apiUrl } from '../states/ApiState';
-export interface PluginPanelState extends PanelType {
- pluginKey: string;
- targetType: string;
- targetId?: string | number | null;
+export type PluginPanelState = {
+ panels: PanelType[];
+};
+
+// Placeholder content for a panel with no content
+function PanelNoContent() {
+ return (
+
+ {t`No content provided for this plugin`}
+
+ );
}
-export function usePluginPanel({
- pluginKey,
- panelName,
+export function usePluginPanels({
targetModel,
targetId
}: {
- pluginKey: string;
- panelName: string;
targetModel: string;
targetId?: string | number | null;
}): PluginPanelState {
- // TODO: Query to fetch the "content" for the plugin
+ const { isFetching, data } = useQuery({
+ queryKey: [targetModel, targetId],
+ queryFn: () => {
+ return api
+ .get(apiUrl(ApiEndpoints.plugin_panel_list), {
+ params: {
+ target_model: targetModel,
+ target_id: targetId
+ }
+ })
+ .then((response: any) => response.data)
+ .catch((error: any) => {
+ console.error('Failed to fetch plugin panels:', error);
+ return [];
+ });
+ }
+ });
- const [loaded, setLoaded] = useState(false);
-
- const { start } = useTimeout(() => setLoaded(true), 5000);
-
- useEffect(() => {
- start();
- console.log('starting timer!');
- }, []);
-
- const content = useMemo(() => {
- return loaded ? (
- 'plugin content loaded!'
- ) : (
-
-
Plugin content goes here...
-
Plugin Key: {pluginKey}
-
Panel Name: {panelName}
-
Target Model: {targetModel}
-
Target ID: {targetId}
-
+ const panels: PanelType[] = useMemo(() => {
+ return (
+ data?.map((panel: any) => {
+ const pluginKey = panel.plugin || 'plugin';
+ return {
+ name: identifierString(`${pluginKey}-${panel.name}`),
+ label: panel.label || t`Plugin Panel`,
+ icon: ,
+ content: panel.content ||
+ };
+ }) ?? []
);
- }, [loaded, pluginKey, panelName, targetModel, targetId]);
+ }, [data]);
return {
- content: content,
- name: panelName,
- pluginKey: pluginKey,
- targetType: targetModel,
- targetId: targetId,
- label: 'A plugin panel',
- icon:
+ panels: panels
};
}
diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx
index 08b330bde8..2372b5d873 100644
--- a/src/frontend/src/pages/part/PartDetail.tsx
+++ b/src/frontend/src/pages/part/PartDetail.tsx
@@ -56,7 +56,8 @@ import {
import { PlaceholderPanel } from '../../components/items/Placeholder';
import NavigationTree from '../../components/nav/NavigationTree';
import { PageDetail } from '../../components/nav/PageDetail';
-import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
+import { PanelType } from '../../components/nav/Panel';
+import { PanelGroup } from '../../components/nav/PanelGroup';
import { formatPriceRange } from '../../defaults/formatters';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
@@ -75,6 +76,7 @@ import {
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance';
+import { usePluginPanels } from '../../hooks/UsePluginPanels';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import { BomTable } from '../../tables/bom/BomTable';
@@ -640,6 +642,15 @@ export default function PartDetail() {
];
}, [id, part, user]);
+ const pluginPanels = usePluginPanels({
+ targetModel: ModelType.part,
+ targetId: id
+ });
+
+ const panels: PanelType[] = useMemo(() => {
+ return [...partPanels, ...pluginPanels.panels];
+ }, [partPanels, pluginPanels]);
+
const breadcrumbs = useMemo(
() => [
{ name: t`Parts`, url: '/part' },
@@ -844,7 +855,7 @@ export default function PartDetail() {
}}
actions={partActions}
/>
-
+
{transferStockItems.modal}
{countStockItems.modal}