diff --git a/CHANGELOG.md b/CHANGELOG.md index a8724e2ec4..8d8169854f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#11778](https://github.com/inventree/InvenTree/pull/11778) adds inline supplier part creation to po line item addition dialog. - [#11772](https://github.com/inventree/InvenTree/pull/11772) the UI now warns if you navigate away from a note panel with unsaved changes - [#11788](https://github.com/inventree/InvenTree/pull/11788) adds support for custom permissions checks on database models defined in plugins. If a model defines a `check_user_permission` classmethod, this will be called to determine if a user has permission to view the model. This is required for plugin models which do not have the required ruleset definitions for the standard permission system. +- [#9570](https://github.com/inventree/InvenTree/pull/9570) adds support for defining primary actions via plugins ### Changed diff --git a/docs/docs/plugins/mixins/ui.md b/docs/docs/plugins/mixins/ui.md index 90329832c2..dc3785d778 100644 --- a/docs/docs/plugins/mixins/ui.md +++ b/docs/docs/plugins/mixins/ui.md @@ -183,6 +183,20 @@ The `get_ui_template_previews` feature type can be used to provide custom templa summary: False members: [] +### Primary Actions + +The `get_ui_primary_actions` method can be used to provide custom primary action, which are rendered in the header of the page, next to the title/name and any status indicators. These primary actions are typically used to provide quick access to common actions related to the current page. + +::: plugin.base.ui.mixins.UserInterfaceMixin.get_ui_primary_actions + options: + show_bases: False + show_root_heading: False + show_root_toc_entry: False + extra: + show_source: True + summary: False + members: [] + ## Plugin Context When rendering certain content in the user interface, the rendering functions are passed a `context` object which contains information about the current page being rendered. The type of the `context` object is defined in the `PluginContext` file: diff --git a/src/backend/InvenTree/plugin/base/ui/mixins.py b/src/backend/InvenTree/plugin/base/ui/mixins.py index 4ab84ef6ca..9aa4a67edd 100644 --- a/src/backend/InvenTree/plugin/base/ui/mixins.py +++ b/src/backend/InvenTree/plugin/base/ui/mixins.py @@ -21,6 +21,7 @@ FeatureType = Literal[ 'template_editor', # Custom template editor 'template_preview', # Custom template preview 'navigation', # Custom navigation items + 'primary_action', # Custom primary action buttons ] @@ -106,6 +107,7 @@ class UserInterfaceMixin: 'panel': self.get_ui_panels, 'template_editor': self.get_ui_template_editors, 'template_preview': self.get_ui_template_previews, + 'primary_action': self.get_ui_primary_actions, } if feature_type in feature_map: @@ -203,3 +205,29 @@ class UserInterfaceMixin: """ # Default implementation returns an empty list return [] + + def get_ui_primary_actions( + self, request: Request, context: dict, **kwargs + ) -> list[UIFeature]: + """Return a list of custom primary action buttons 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 primary action buttons to be injected into the UI + """ + # Sample code to render conditional primary action button based on context + # items = [] + # if self.my_assert_function(context): + # items.append({ + # 'key': 'sample-primary-action', + # 'title': 'Sample Primary Action', + # 'icon': 'ti:plus:outline', + # 'options': {'url': '/core/sample-primary-action/', 'color': 'orange'}, + # }) + # return items + + # Default implementation returns an empty list + return [] diff --git a/src/backend/InvenTree/plugin/base/ui/tests.py b/src/backend/InvenTree/plugin/base/ui/tests.py index e38e64a856..0a4a94977b 100644 --- a/src/backend/InvenTree/plugin/base/ui/tests.py +++ b/src/backend/InvenTree/plugin/base/ui/tests.py @@ -233,3 +233,13 @@ class UserInterfaceMixinTests(InvenTreeAPITestCase): 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') + + def test_ui_primary_actions(self): + """Test that the sample UI plugin provides custom primary actions.""" + response = self.get( + reverse('api-plugin-ui-feature-list', kwargs={'feature': 'primary_action'}) + ) + self.assertEqual(1, len(response.data)) + self.assertEqual(response.data[0]['plugin_name'], 'sampleui') + self.assertEqual(response.data[0]['key'], 'sample-primary-action') + self.assertEqual(response.data[0]['title'], 'Sample Primary Action') 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 70c47f220a..09bdbb7f37 100644 --- a/src/backend/InvenTree/plugin/samples/integration/user_interface_sample.py +++ b/src/backend/InvenTree/plugin/samples/integration/user_interface_sample.py @@ -226,6 +226,17 @@ class SampleUserInterfacePlugin(SettingsMixin, UserInterfaceMixin, InvenTreePlug } ] + def get_ui_primary_actions(self, request, context, **kwargs): + """Return a list of custom primary action buttons.""" + return [ + { + 'key': 'sample-primary-action', + 'title': 'Sample Primary Action', + 'icon': 'ti:plus:outline', + 'options': {'url': '/core/sample-primary-action/', 'color': 'orange'}, + } + ] + 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/buttons/PrimaryActionButton.tsx b/src/frontend/src/components/buttons/PrimaryActionButton.tsx index 275fd21379..293d3b0b8b 100644 --- a/src/frontend/src/components/buttons/PrimaryActionButton.tsx +++ b/src/frontend/src/components/buttons/PrimaryActionButton.tsx @@ -12,7 +12,8 @@ export default function PrimaryActionButton({ icon, color, hidden, - onClick + onClick, + leftSection }: Readonly<{ title: string; tooltip?: string; @@ -20,6 +21,7 @@ export default function PrimaryActionButton({ color?: string; hidden?: boolean; onClick: () => void; + leftSection?: React.ReactNode; }>) { if (hidden) { return null; @@ -28,7 +30,7 @@ export default function PrimaryActionButton({ return (