From e39234865413649f80f49570159ae018cca98f0f Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 22 May 2026 00:17:11 +1000 Subject: [PATCH 01/10] [UI] Calendar tooltips (#11980) * Allow tooltips for calendar entries * Render tooltip for purchase order calendar * Calendar tooltip renderers * Add tooltip for build calendar * Add key to filter list * Display responsible owner --- .../src/components/calendar/Calendar.tsx | 48 ++++++++++++++- .../src/components/calendar/OrderCalendar.tsx | 6 +- .../calendar/OrderCalendarToolTip.tsx | 61 +++++++++++++++++++ .../src/components/render/Company.tsx | 8 +-- src/frontend/src/pages/build/BuildIndex.tsx | 15 ++++- .../src/pages/purchasing/PurchasingIndex.tsx | 15 ++++- src/frontend/src/pages/sales/SalesIndex.tsx | 26 +++++++- .../src/tables/InvenTreeTableHeader.tsx | 1 + 8 files changed, 166 insertions(+), 14 deletions(-) create mode 100644 src/frontend/src/components/calendar/OrderCalendarToolTip.tsx diff --git a/src/frontend/src/components/calendar/Calendar.tsx b/src/frontend/src/components/calendar/Calendar.tsx index 8b80347fce..dcddcdcfd1 100644 --- a/src/frontend/src/components/calendar/Calendar.tsx +++ b/src/frontend/src/components/calendar/Calendar.tsx @@ -1,4 +1,8 @@ -import type { CalendarOptions, DatesSetArg } from '@fullcalendar/core'; +import type { + CalendarOptions, + DatesSetArg, + EventContentArg +} from '@fullcalendar/core'; import allLocales from '@fullcalendar/core/locales-all'; import dayGridPlugin from '@fullcalendar/daygrid'; import interactionPlugin from '@fullcalendar/interaction'; @@ -15,6 +19,7 @@ import { Box, Button, Group, + HoverCard, Indicator, LoadingOverlay, Popover, @@ -29,7 +34,13 @@ import { IconDownload, IconFilter } from '@tabler/icons-react'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { + type ReactNode, + useCallback, + useEffect, + useMemo, + useState +} from 'react'; import { useShallow } from 'zustand/react/shallow'; import { defaultLocale, @@ -44,6 +55,7 @@ export interface InvenTreeCalendarProps extends CalendarOptions { enableDownload?: boolean; enableFilters?: boolean; enableSearch?: boolean; + eventTooltipContent?: (event: EventContentArg) => ReactNode; filters?: TableFilter[]; isLoading?: boolean; state: CalendarState; @@ -53,6 +65,7 @@ export default function Calendar({ enableDownload, enableFilters = false, enableSearch, + eventTooltipContent, isLoading, filters, state, @@ -112,6 +125,36 @@ export default function Calendar({ [calendarProps.datesSet, state.ref, state.setMonthName] ); + const wrappedEventContent = useCallback( + (arg: EventContentArg) => { + const inner = + typeof calendarProps.eventContent === 'function' + ? calendarProps.eventContent(arg, null) + : (calendarProps.eventContent ?? null); + + if (!eventTooltipContent) return inner; + + const tooltip = eventTooltipContent(arg); + + if (!tooltip) return inner; + + return ( + + +
{inner}
+
+ {tooltip} +
+ ); + }, + [calendarProps.eventContent, eventTooltipContent] + ); + return ( <> {state.exportModal.modal} @@ -217,6 +260,7 @@ export default function Calendar({ footerToolbar={false} {...calendarProps} datesSet={datesSet} + eventContent={wrappedEventContent} /> diff --git a/src/frontend/src/components/calendar/OrderCalendar.tsx b/src/frontend/src/components/calendar/OrderCalendar.tsx index 9c3523653e..95fd76abe3 100644 --- a/src/frontend/src/components/calendar/OrderCalendar.tsx +++ b/src/frontend/src/components/calendar/OrderCalendar.tsx @@ -46,12 +46,14 @@ export default function OrderCalendar({ model, role, params, - filters + filters, + tooltip }: { model: ModelType; role: UserRoles; params: Record; filters?: TableFilter[]; + tooltip?: (event: EventContentArg) => React.ReactNode; }) { const navigate = useNavigate(); const user = useUserState(); @@ -106,6 +108,7 @@ export default function OrderCalendar({ const end: string = order.target_date || start; return { + order: order, id: order.pk, title: order.reference, description: order.description, @@ -209,6 +212,7 @@ export default function OrderCalendar({ state={calendarState} filters={calendarFilters} editable={true} + eventTooltipContent={tooltip} eventContent={renderOrder} eventClick={onClickOrder} eventChange={onEditOrder} diff --git a/src/frontend/src/components/calendar/OrderCalendarToolTip.tsx b/src/frontend/src/components/calendar/OrderCalendarToolTip.tsx new file mode 100644 index 0000000000..dba5259358 --- /dev/null +++ b/src/frontend/src/components/calendar/OrderCalendarToolTip.tsx @@ -0,0 +1,61 @@ +import type { EventContentArg } from '@fullcalendar/core'; +import type { ModelType } from '@lib/enums/ModelType'; +import { resolveItem } from '@lib/functions/Conversion'; +import { t } from '@lingui/core/macro'; +import { Badge, Divider, Group, Stack, Text } from '@mantine/core'; +import { formatDate } from '../../defaults/formatters'; +import { RenderInstance } from '../render/Instance'; +import { RenderOwner } from '../render/User'; + +export default function OrderCalendarToolTip({ + event, + instanceLookup, + modelType +}: { + event: EventContentArg; + instanceLookup: string; + modelType: ModelType; +}) { + // Extract the order instance from the event + const order = event?.event?._def?.extendedProps?.order; + + const instance = resolveItem(order, instanceLookup); + + if (!order) return null; + + return ( + + + + + + {order.reference} + + {order.description || order.title} + + {order.start_date && ( + + {t`Start Date`} + {formatDate(order.start_date)} + + )} + {order.target_date && ( + + {t`Target Date`} + {formatDate(order.target_date)} + {order.overdue && ( + + {t`Overdue`} + + )} + + )} + {order.responsible && ( + + {t`Responsible`} + + + )} + + ); +} diff --git a/src/frontend/src/components/render/Company.tsx b/src/frontend/src/components/render/Company.tsx index e8050d5f12..09aeee04cc 100644 --- a/src/frontend/src/components/render/Company.tsx +++ b/src/frontend/src/components/render/Company.tsx @@ -50,11 +50,11 @@ export function RenderCompany( return ( ); diff --git a/src/frontend/src/pages/build/BuildIndex.tsx b/src/frontend/src/pages/build/BuildIndex.tsx index 287b1d9cc4..f52441923d 100644 --- a/src/frontend/src/pages/build/BuildIndex.tsx +++ b/src/frontend/src/pages/build/BuildIndex.tsx @@ -6,14 +6,16 @@ import { IconTable, IconTools } from '@tabler/icons-react'; -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; +import type { EventContentArg } from '@fullcalendar/core'; import { ModelType, PluginPanelKey } from '@lib/enums/ModelType'; import { UserRoles } from '@lib/enums/Roles'; import type { TableFilter } from '@lib/types/Filters'; import type { PanelType } from '@lib/types/Panel'; import { useLocalStorage } from '@mantine/hooks'; import OrderCalendar from '../../components/calendar/OrderCalendar'; +import OrderCalendarToolTip from '../../components/calendar/OrderCalendarToolTip'; import PermissionDenied from '../../components/errors/PermissionDenied'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup } from '../../components/panels/PanelGroup'; @@ -34,12 +36,21 @@ function BuildOrderCalendar() { }); }, [globalSettings]); + const renderTooltip = useCallback((event: EventContentArg) => { + return OrderCalendarToolTip({ + event: event, + modelType: ModelType.part, + instanceLookup: 'part_detail' + }); + }, []); + return ( ); } diff --git a/src/frontend/src/pages/purchasing/PurchasingIndex.tsx b/src/frontend/src/pages/purchasing/PurchasingIndex.tsx index 3087c9b530..bdaa271c0e 100644 --- a/src/frontend/src/pages/purchasing/PurchasingIndex.tsx +++ b/src/frontend/src/pages/purchasing/PurchasingIndex.tsx @@ -10,13 +10,15 @@ import { IconShoppingCart, IconTable } from '@tabler/icons-react'; -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; +import type { EventContentArg } from '@fullcalendar/core'; import { ModelType, PluginPanelKey } from '@lib/enums/ModelType'; import { UserRoles } from '@lib/enums/Roles'; import type { TableFilter } from '@lib/index'; import { useLocalStorage } from '@mantine/hooks'; import OrderCalendar from '../../components/calendar/OrderCalendar'; +import OrderCalendarToolTip from '../../components/calendar/OrderCalendarToolTip'; import PermissionDenied from '../../components/errors/PermissionDenied'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup } from '../../components/panels/PanelGroup'; @@ -37,12 +39,21 @@ function PurchaseOrderCalendar() { return PurchaseOrderFilters({ includeDateFilters: false }); }, []); + const renderTooltip = useCallback((event: EventContentArg) => { + return OrderCalendarToolTip({ + event: event, + modelType: ModelType.company, + instanceLookup: 'supplier_detail' + }); + }, []); + return ( ); } diff --git a/src/frontend/src/pages/sales/SalesIndex.tsx b/src/frontend/src/pages/sales/SalesIndex.tsx index a36fde83da..0f1c0879b0 100644 --- a/src/frontend/src/pages/sales/SalesIndex.tsx +++ b/src/frontend/src/pages/sales/SalesIndex.tsx @@ -9,13 +9,15 @@ import { IconTruckDelivery, IconTruckReturn } from '@tabler/icons-react'; -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; +import type { EventContentArg } from '@fullcalendar/core'; import { ModelType, PluginPanelKey } from '@lib/enums/ModelType'; import { UserRoles } from '@lib/enums/Roles'; import type { TableFilter } from '@lib/index'; import { useLocalStorage } from '@mantine/hooks'; import OrderCalendar from '../../components/calendar/OrderCalendar'; +import OrderCalendarToolTip from '../../components/calendar/OrderCalendarToolTip'; import PermissionDenied from '../../components/errors/PermissionDenied'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup } from '../../components/panels/PanelGroup'; @@ -35,12 +37,21 @@ function SalesOrderCalendar() { return SalesOrderFilters({ includeDateFilters: false }); }, []); + const renderTooltip = useCallback((event: EventContentArg) => { + return OrderCalendarToolTip({ + event: event, + modelType: ModelType.company, + instanceLookup: 'customer_detail' + }); + }, []); + return ( ); } @@ -50,12 +61,21 @@ const ReturnOrderCalendar = () => { return SalesOrderFilters({ includeDateFilters: false }); }, []); + const renderTooltip = useCallback((event: EventContentArg) => { + return OrderCalendarToolTip({ + event: event, + modelType: ModelType.company, + instanceLookup: 'customer_detail' + }); + }, []); + return ( ); }; diff --git a/src/frontend/src/tables/InvenTreeTableHeader.tsx b/src/frontend/src/tables/InvenTreeTableHeader.tsx index 612b6ba06d..2defcb7a22 100644 --- a/src/frontend/src/tables/InvenTreeTableHeader.tsx +++ b/src/frontend/src/tables/InvenTreeTableHeader.tsx @@ -276,6 +276,7 @@ export default function InvenTreeTableHeader({ {tableState.filterSet.activeFilters?.map((filter) => ( From 65d15a59451858221ba13a035c476be48c2c8b3a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 08:19:01 +1000 Subject: [PATCH 02/10] chore(deps): bump idna from 3.13 to 3.15 in /contrib/dev_reqs (#11970) * chore(deps): bump idna from 3.13 to 3.15 in /contrib/dev_reqs Bumps [idna](https://github.com/kjd/idna) from 3.13 to 3.15. - [Release notes](https://github.com/kjd/idna/releases) - [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.md) - [Commits](https://github.com/kjd/idna/compare/v3.13...v3.15) --- updated-dependencies: - dependency-name: idna dependency-version: '3.15' dependency-type: indirect ... Signed-off-by: dependabot[bot] * fix style --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Matthias Mair --- contrib/dev_reqs/requirements.txt | 6 +++--- docs/requirements.txt | 6 +++--- src/backend/requirements-3.14.txt | 6 +++--- src/backend/requirements-dev-3.14.txt | 6 +++--- src/backend/requirements-dev.txt | 6 +++--- src/backend/requirements.txt | 6 +++--- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/contrib/dev_reqs/requirements.txt b/contrib/dev_reqs/requirements.txt index a783698ee4..0591b6d8a6 100644 --- a/contrib/dev_reqs/requirements.txt +++ b/contrib/dev_reqs/requirements.txt @@ -139,9 +139,9 @@ charset-normalizer==3.4.7 \ # via # -c src/backend/requirements.txt # requests -idna==3.13 \ - --hash=sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242 \ - --hash=sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3 +idna==3.15 \ + --hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \ + --hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc # via # -c src/backend/requirements.txt # requests diff --git a/docs/requirements.txt b/docs/requirements.txt index df1b00c4a1..4248102d6e 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -227,9 +227,9 @@ httpx==0.28.1 \ --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad # via neoteroi-mkdocs -idna==3.13 \ - --hash=sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242 \ - --hash=sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3 +idna==3.15 \ + --hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \ + --hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc # via # -c src/backend/requirements.txt # anyio diff --git a/src/backend/requirements-3.14.txt b/src/backend/requirements-3.14.txt index c331b69655..96528aea96 100644 --- a/src/backend/requirements-3.14.txt +++ b/src/backend/requirements-3.14.txt @@ -977,9 +977,9 @@ icalendar==7.0.3 \ # via # -c src/backend/requirements.txt # django-ical -idna==3.13 \ - --hash=sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242 \ - --hash=sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3 +idna==3.15 \ + --hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \ + --hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc # via # -c src/backend/requirements.txt # django-anymail diff --git a/src/backend/requirements-dev-3.14.txt b/src/backend/requirements-dev-3.14.txt index 9c7503d9dc..a0f377ed8f 100644 --- a/src/backend/requirements-dev-3.14.txt +++ b/src/backend/requirements-dev-3.14.txt @@ -449,9 +449,9 @@ gprof2dot==2025.4.14 \ --hash=sha256:0742e4c0b4409a5e8777e739388a11e1ed3750be86895655312ea7c20bd0090e \ --hash=sha256:35743e2d2ca027bf48fa7cba37021aaf4a27beeae1ae8e05a50b55f1f921a6ce # via django-silk -idna==3.13 \ - --hash=sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242 \ - --hash=sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3 +idna==3.15 \ + --hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \ + --hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc # via # -c src/backend/requirements-3.14.txt # -c src/backend/requirements.txt diff --git a/src/backend/requirements-dev.txt b/src/backend/requirements-dev.txt index 9aa16fb90e..cdb18aede7 100644 --- a/src/backend/requirements-dev.txt +++ b/src/backend/requirements-dev.txt @@ -443,9 +443,9 @@ gprof2dot==2025.4.14 \ --hash=sha256:0742e4c0b4409a5e8777e739388a11e1ed3750be86895655312ea7c20bd0090e \ --hash=sha256:35743e2d2ca027bf48fa7cba37021aaf4a27beeae1ae8e05a50b55f1f921a6ce # via django-silk -idna==3.13 \ - --hash=sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242 \ - --hash=sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3 +idna==3.15 \ + --hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \ + --hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc # via # -c src/backend/requirements.txt # requests diff --git a/src/backend/requirements.txt b/src/backend/requirements.txt index d515cb039e..2d730180a4 100644 --- a/src/backend/requirements.txt +++ b/src/backend/requirements.txt @@ -868,9 +868,9 @@ icalendar==7.0.3 \ --hash=sha256:8c9fea6d3a89671bba8b6938d8565b4d0ec465c6a2796ef0f92790dcb9e627cd \ --hash=sha256:95027ece087ab87184d765f03761f25875821f74cdd18d3b57e9c868216d8fde # via django-ical -idna==3.13 \ - --hash=sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242 \ - --hash=sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3 +idna==3.15 \ + --hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \ + --hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc # via # django-anymail # requests From f27b9b5443af0a99f87066547c80fea3be652b0f Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Fri, 22 May 2026 00:20:07 +0200 Subject: [PATCH 03/10] feat(frontend): Add option for plugins to add header actions (#9570) * [FR] PUI - Add option for plugins to add header actions Fixes #8593 * fix parsing * fix merge * reduce diff * fix sample implementation * add support for icons and colors in primary actions * add changelog entry * add docs * add more detailed sample text * pass location into context * fix test --------- Co-authored-by: Oliver --- CHANGELOG.md | 1 + docs/docs/plugins/mixins/ui.md | 14 ++++++ .../InvenTree/plugin/base/ui/mixins.py | 28 ++++++++++++ src/backend/InvenTree/plugin/base/ui/tests.py | 10 +++++ .../integration/user_interface_sample.py | 11 +++++ .../buttons/PrimaryActionButton.tsx | 6 ++- .../src/components/nav/PageDetail.tsx | 44 ++++++++++++++++++- .../components/plugins/PluginUIFeature.tsx | 3 +- .../plugins/PluginUIFeatureTypes.ts | 8 ++++ src/frontend/tests/pages/pui_stock.spec.ts | 7 +-- 10 files changed, 122 insertions(+), 10 deletions(-) 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 (