From 6b0a082b5a7fad08cb5f2720f0fc632b2518f250 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Sun, 20 Apr 2025 03:22:58 +0200 Subject: [PATCH] feat: New / Refactor "Nav Mixin" (#9283) * [FR/P-UI] New / Refactor "Nav Mixin" Fixes #5269 * remove logging * fix sample item that causes issues * Add test coverage * Update src/frontend/src/components/plugins/PluginUIFeatureTypes.ts Co-authored-by: Lukas <76838159+wolflu05@users.noreply.github.com> * [FR/P-UI] New / Refactor "Nav Mixin" Fixes #5269 * fix style * remove requirement for source * fix import * bump api version --------- Co-authored-by: Lukas <76838159+wolflu05@users.noreply.github.com> --- .../InvenTree/InvenTree/api_version.py | 5 ++- .../InvenTree/plugin/base/ui/mixins.py | 17 +++++++++ .../InvenTree/plugin/base/ui/serializers.py | 2 +- src/backend/InvenTree/plugin/base/ui/tests.py | 10 +++++ .../integration/user_interface_sample.py | 11 ++++++ .../src/components/calendar/OrderCalendar.tsx | 3 +- src/frontend/src/components/nav/Header.tsx | 37 +++++++++++++++---- .../components/plugins/PluginUIFeature.tsx | 3 +- .../plugins/PluginUIFeatureTypes.ts | 8 ++++ 9 files changed, 84 insertions(+), 12 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index a8d72c2474..de04146469 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,13 +1,16 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 338 +INVENTREE_API_VERSION = 339 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v339 -> 2025-04-15 : https://github.com/inventree/InvenTree/pull/9283 + - Remove need for source in /plugins/ui/features + v338 -> 2025-04-15 : https://github.com/inventree/InvenTree/pull/9333 - Adds oAuth2 support for the API diff --git a/src/backend/InvenTree/plugin/base/ui/mixins.py b/src/backend/InvenTree/plugin/base/ui/mixins.py index e1ff054a59..67dab02ad4 100644 --- a/src/backend/InvenTree/plugin/base/ui/mixins.py +++ b/src/backend/InvenTree/plugin/base/ui/mixins.py @@ -19,6 +19,7 @@ FeatureType = Literal[ 'panel', # Custom panels 'template_editor', # Custom template editor 'template_preview', # Custom template preview + 'navigation', # Custom navigation items ] @@ -102,6 +103,7 @@ class UserInterfaceMixin: 'panel': self.get_ui_panels, 'template_editor': self.get_ui_template_editors, 'template_preview': self.get_ui_template_previews, + 'navigation': self.get_ui_navigation_items, } if feature_type in feature_map: @@ -169,3 +171,18 @@ class UserInterfaceMixin: """ # Default implementation returns an empty list return [] + + def get_ui_navigation_items( + self, request: Request, context: dict, **kwargs + ) -> list[UIFeature]: + """Return a list of custom navigation items to be injected into the UI. + + Args: + request: HTTPRequest object (including user information) + context: Additional context data provided by the UI (query parameters) + + Returns: + list: A list of custom navigation items 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 index 99a3126399..c60aa322a6 100644 --- a/src/backend/InvenTree/plugin/base/ui/serializers.py +++ b/src/backend/InvenTree/plugin/base/ui/serializers.py @@ -61,5 +61,5 @@ class PluginUIFeatureSerializer(serializers.Serializer): context = serializers.DictField(label=_('Feature Context'), default=None) source = serializers.CharField( - label=_('Feature Source (javascript)'), required=True, allow_blank=False + label=_('Feature Source (javascript)'), required=False, allow_blank=True ) diff --git a/src/backend/InvenTree/plugin/base/ui/tests.py b/src/backend/InvenTree/plugin/base/ui/tests.py index 2ff09e8960..f372374bb4 100644 --- a/src/backend/InvenTree/plugin/base/ui/tests.py +++ b/src/backend/InvenTree/plugin/base/ui/tests.py @@ -223,3 +223,13 @@ class UserInterfaceMixinTests(InvenTreeAPITestCase): # Set the setting back to True for subsequent tests InvenTreeSetting.set_setting('ENABLE_PLUGINS_INTERFACE', True, change_user=None) + + def test_ui_navigation_items(self): + """Test that the sample UI plugin provides custom navigation items.""" + response = self.get( + reverse('api-plugin-ui-feature-list', kwargs={'feature': 'navigation'}) + ) + self.assertEqual(1, len(response.data)) + self.assertEqual(response.data[0]['plugin_name'], 'sampleui') + self.assertEqual(response.data[0]['key'], 'sample-nav-item') + self.assertEqual(response.data[0]['title'], 'Sample Nav Item') diff --git a/src/backend/InvenTree/plugin/samples/integration/user_interface_sample.py b/src/backend/InvenTree/plugin/samples/integration/user_interface_sample.py index c475a68f5a..ba906cf225 100644 --- a/src/backend/InvenTree/plugin/samples/integration/user_interface_sample.py +++ b/src/backend/InvenTree/plugin/samples/integration/user_interface_sample.py @@ -197,6 +197,17 @@ class SampleUserInterfacePlugin(SettingsMixin, UserInterfaceMixin, InvenTreePlug } ] + def get_ui_navigation_items(self, request, context, **kwargs): + """Return a list of custom navigation items.""" + return [ + { + 'key': 'sample-nav-item', + 'title': 'Sample Nav Item', + 'icon': 'ti:menu', + 'options': {'url': '/sample/page/'}, + } + ] + def get_admin_context(self) -> dict: """Return custom context data which can be rendered in the admin panel.""" return {'apple': 'banana', 'foo': 'bar', 'hello': 'world'} diff --git a/src/frontend/src/components/calendar/OrderCalendar.tsx b/src/frontend/src/components/calendar/OrderCalendar.tsx index b7ae28abd3..a213358d36 100644 --- a/src/frontend/src/components/calendar/OrderCalendar.tsx +++ b/src/frontend/src/components/calendar/OrderCalendar.tsx @@ -7,8 +7,7 @@ import { ModelInformationDict } from '@lib/enums/ModelInformation'; import type { ModelType } from '@lib/enums/ModelType'; import type { UserRoles } from '@lib/enums/Roles'; import { apiUrl } from '@lib/functions/Api'; -import { getDetailUrl } from '@lib/functions/Navigation'; -import { navigateToLink } from '@lib/functions/Navigation'; +import { getDetailUrl, navigateToLink } from '@lib/functions/Navigation'; import type { TableFilter } from '@lib/types/Filters'; import { t } from '@lingui/core/macro'; import { ActionIcon, Group, Text } from '@mantine/core'; diff --git a/src/frontend/src/components/nav/Header.tsx b/src/frontend/src/components/nav/Header.tsx index 8d35bb908b..0a0920f032 100644 --- a/src/frontend/src/components/nav/Header.tsx +++ b/src/frontend/src/components/nav/Header.tsx @@ -1,3 +1,7 @@ +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { apiUrl } from '@lib/functions/Api'; +import { navigateToLink } from '@lib/functions/Navigation'; +import { t } from '@lingui/core/macro'; import { ActionIcon, Container, @@ -12,13 +16,10 @@ import { IconBell, IconSearch } from '@tabler/icons-react'; import { useQuery } from '@tanstack/react-query'; import { type ReactNode, useEffect, useMemo, useState } from 'react'; import { useMatch, useNavigate } from 'react-router-dom'; - -import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; -import { apiUrl } from '@lib/functions/Api'; -import { navigateToLink } from '@lib/functions/Navigation'; -import { t } from '@lingui/core/macro'; import { api } from '../../App'; +import type { NavigationUIFeature } from '../../components/plugins/PluginUIFeatureTypes'; import { getNavTabs } from '../../defaults/links'; +import { usePluginUIFeature } from '../../hooks/UsePluginUIFeature'; import * as classes from '../../main.css'; import { useServerApiState } from '../../states/ApiState'; import { useLocalState } from '../../states/LocalState'; @@ -180,10 +181,18 @@ function NavTabs() { [userSettings] ); + const extraNavs = usePluginUIFeature({ + featureType: 'navigation', + context: {} + }); + const tabs: ReactNode[] = useMemo(() => { const _tabs: ReactNode[] = []; - navTabs.forEach((tab) => { + const mainNavTabs = getNavTabs(user); + + // static content + mainNavTabs.forEach((tab) => { if (tab.role && !user.hasViewRole(tab.role)) { return; } @@ -206,9 +215,23 @@ function NavTabs() { ); }); + // dynamic content + extraNavs.forEach((nav) => { + _tabs.push( + + navigateToLink(nav.options.options.url, navigate, event) + } + > + {nav.options.title} + + ); + }); return _tabs; - }, [navTabs, user, withIcons]); + }, [extraNavs, navTabs, user, withIcons]); return (