diff --git a/CHANGELOG.md b/CHANGELOG.md index c45d925135..bb9e2e4d85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- [#13172](https://github.com/inventree/InvenTree/pull/13172) passes the `target_model` and `target_id` of the current page to plugin primary actions, via the `context` argument of `get_ui_primary_actions`. + ### Removed - [#13101](https://github.com/inventree/InvenTree/pull/13101) removes the StockTracking model and its associated API endpoints. Stock history tracking is now handled by the global audit log. diff --git a/docs/docs/plugins/mixins/ui.md b/docs/docs/plugins/mixins/ui.md index 0ef327bdbb..7058d4e975 100644 --- a/docs/docs/plugins/mixins/ui.md +++ b/docs/docs/plugins/mixins/ui.md @@ -187,6 +187,14 @@ The `get_ui_template_previews` feature type can be used to provide custom templa 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. +The `context` dictionary provided to this method contains the following keys: + +| Key | Description | +| --- | --- | +| `location` | The URL path of the current page | +| `target_model` | The model type of the current page (e.g. `part`), or the page key for index pages (e.g. `purchasing`). Not provided for all pages. | +| `target_id` | The primary key of the instance displayed on the current page. Not provided for all pages. | + ::: plugin.base.ui.mixins.UserInterfaceMixin.get_ui_primary_actions options: show_bases: False diff --git a/src/backend/InvenTree/plugin/base/ui/mixins.py b/src/backend/InvenTree/plugin/base/ui/mixins.py index 635bceb934..2851f15e43 100644 --- a/src/backend/InvenTree/plugin/base/ui/mixins.py +++ b/src/backend/InvenTree/plugin/base/ui/mixins.py @@ -217,12 +217,17 @@ class UserInterfaceMixin: request: HTTPRequest object (including user information) context: Additional context data provided by the UI (query parameters) + The context data provided by the UI includes: + - location: The URL path of the current page + - target_model: The model type (or page key) of the current page, e.g. 'part' or 'purchasing' (if available) + - target_id: The primary key of the instance displayed on the current page (if available) + 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): + # if context.get('target_model') == 'part' and context.get('target_id'): # items.append({ # 'key': 'sample-primary-action', # 'title': 'Sample Primary Action', diff --git a/src/frontend/src/components/nav/PageDetail.tsx b/src/frontend/src/components/nav/PageDetail.tsx index cc21b487d3..242ff1fd0e 100644 --- a/src/frontend/src/components/nav/PageDetail.tsx +++ b/src/frontend/src/components/nav/PageDetail.tsx @@ -1,6 +1,7 @@ import { Group, Paper, Space, Stack, Text } from '@mantine/core'; import { StylishText } from '@lib/components/StylishText'; +import type { ModelType, PluginPanelKey } from '@lib/enums/ModelType'; import { useInvenTreeHotkeys } from '@lib/functions/Events'; import { shortenString } from '@lib/functions/String'; import { t } from '@lingui/core/macro'; @@ -30,13 +31,16 @@ interface PageDetailInterface { actions?: ReactNode[]; editAction?: () => void; editEnabled?: boolean; + model?: ModelType | PluginPanelKey; + id?: number | string | null; } /** * Construct a "standard" page detail for common display between pages. * * @param breadcrumbs - The breadcrumbs to display (optional) - * @param + * @param model - The model type (or plugin panel key) for this page, passed to plugin primary actions (optional) + * @param id - The primary key of the instance displayed on this page, passed to plugin primary actions (optional) */ export function PageDetail({ title, @@ -50,7 +54,9 @@ export function PageDetail({ breadcrumbAction, actions, editAction, - editEnabled + editEnabled, + model, + id }: Readonly) { const userSettings = useUserSettingsState(); const navigate = useNavigate(); @@ -102,7 +108,11 @@ export function PageDetail({ const extraActions = usePluginUIFeature({ featureType: 'primary_action', - context: { location: location.pathname } + context: { + location: location.pathname, + target_model: model, + target_id: id + } }); // action caching diff --git a/src/frontend/src/components/plugins/PluginUIFeatureTypes.ts b/src/frontend/src/components/plugins/PluginUIFeatureTypes.ts index 9138cc627c..d6dde4092f 100644 --- a/src/frontend/src/components/plugins/PluginUIFeatureTypes.ts +++ b/src/frontend/src/components/plugins/PluginUIFeatureTypes.ts @@ -1,4 +1,4 @@ -import type { ModelType } from '@lib/enums/ModelType'; +import type { ModelType, PluginPanelKey } from '@lib/enums/ModelType'; import type { InvenTreeIconType } from '@lib/types/Icons'; import type { InvenTreePluginContext } from '@lib/types/Plugins'; import type { TemplateI } from '../../tables/settings/TemplateTable'; @@ -87,7 +87,11 @@ export type NavigationUIFeature = { export type PrimaryActionUIFeature = { featureType: 'primary_action'; - requestContext: {}; + requestContext: { + location: string; + target_model?: ModelType | PluginPanelKey; + target_id?: number | string | null; + }; responseOptions: PluginUIFeature; featureContext: {}; featureReturnType: undefined; diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx index 493b36f1da..fcbe3e2043 100644 --- a/src/frontend/src/pages/build/BuildDetail.tsx +++ b/src/frontend/src/pages/build/BuildDetail.tsx @@ -605,6 +605,8 @@ export default function BuildDetail() { - + ) { > - + } diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index 6e4253df0f..d1ff84ed9d 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -834,6 +834,8 @@ export default function PartDetail() { > - + - + } diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx index 33c4ec598d..3c7710ca5a 100644 --- a/src/frontend/src/pages/stock/StockDetail.tsx +++ b/src/frontend/src/pages/stock/StockDetail.tsx @@ -747,6 +747,8 @@ export default function StockDetail() { > { export const tableSearch = async (page: Page, query: string) => { await page.getByLabel('table-search-input').fill(query); - await page.waitForTimeout(300); + // Wait for longer than the SearchInput debounce (500ms) + await page.waitForTimeout(600); await page.waitForLoadState('networkidle'); }; export const clearTableSearch = async (page: Page) => { await page.getByLabel('table-search-input').clear(); - await page.waitForTimeout(300); + await page.waitForTimeout(600); await page.waitForLoadState('networkidle'); }; diff --git a/src/frontend/tests/pages/pui_sales.spec.ts b/src/frontend/tests/pages/pui_sales.spec.ts index 444e70669d..c754c384b1 100644 --- a/src/frontend/tests/pages/pui_sales.spec.ts +++ b/src/frontend/tests/pages/pui_sales.spec.ts @@ -423,7 +423,11 @@ test('Sales Orders - Shipment Row Actions', async ({ browser }) => { // View the parent sales order from the global shipments table await navigate(page, 'sales/index/shipments'); await clearTableFilters(page); - await page.getByLabel('table-search-input').fill(shipmentReference); + await tableSearch(page, shipmentReference); + + // Wait for the (debounced) search to be applied before opening the row menu, + // otherwise the table re-renders underneath the open menu + await expect(page.locator('tbody tr')).toHaveCount(1); await clickOnRowMenu(page.getByRole('cell', { name: shipmentReference })); await page.getByRole('menuitem', { name: 'View Sales Order' }).click(); await page.getByText('Sales Order: SO0007').first().waitFor();