mirror of
https://github.com/inventree/InvenTree.git
synced 2026-06-12 03:28:37 +00:00
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 <oliver.henry.walters@gmail.com>
This commit is contained in:
@@ -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 (
|
||||
<Tooltip label={tooltip ?? title} position='bottom' hidden={!tooltip}>
|
||||
<Button
|
||||
leftSection={icon && <InvenTreeIcon icon={icon} />}
|
||||
leftSection={leftSection ?? (icon && <InvenTreeIcon icon={icon} />)}
|
||||
color={color}
|
||||
radius='sm'
|
||||
p='xs'
|
||||
|
||||
@@ -4,8 +4,13 @@ import { useHotkeys } from '@mantine/hooks';
|
||||
import { StylishText } from '@lib/components/StylishText';
|
||||
import { shortenString } from '@lib/functions/String';
|
||||
import { Fragment, type ReactNode, useMemo } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { usePluginUIFeature } from '../../hooks/UsePluginUIFeature';
|
||||
import { useUserSettingsState } from '../../states/SettingsStates';
|
||||
import PrimaryActionButton from '../buttons/PrimaryActionButton';
|
||||
import { ApiImage } from '../images/ApiImage';
|
||||
import { ApiIcon } from '../items/ApiIcon';
|
||||
import type { PrimaryActionUIFeature } from '../plugins/PluginUIFeatureTypes';
|
||||
import { type Breadcrumb, BreadcrumbList } from './BreadcrumbList';
|
||||
import PageTitle from './PageTitle';
|
||||
|
||||
@@ -45,6 +50,8 @@ export function PageDetail({
|
||||
editEnabled
|
||||
}: Readonly<PageDetailInterface>) {
|
||||
const userSettings = useUserSettingsState();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
useHotkeys([
|
||||
[
|
||||
'mod+E',
|
||||
@@ -86,6 +93,39 @@ export function PageDetail({
|
||||
}
|
||||
}, [breadcrumbs, last_crumb, userSettings]);
|
||||
|
||||
const extraActions = usePluginUIFeature<PrimaryActionUIFeature>({
|
||||
featureType: 'primary_action',
|
||||
context: { location: location.pathname }
|
||||
});
|
||||
|
||||
// action caching
|
||||
const computedActions = useMemo(() => {
|
||||
const extraActionArray: ReactNode[] = extraActions.map((action) => {
|
||||
const { options: opts, func } = action;
|
||||
const { title, icon, context, options } = opts;
|
||||
|
||||
const click = () => {
|
||||
const url = options?.url;
|
||||
if (url) {
|
||||
navigate(url);
|
||||
} else if (func) {
|
||||
func(context);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PrimaryActionButton
|
||||
title={title}
|
||||
leftSection={<ApiIcon name={icon as string} />}
|
||||
color={options?.color}
|
||||
onClick={click}
|
||||
key={title}
|
||||
/>
|
||||
);
|
||||
});
|
||||
return [...(extraActionArray ?? []), ...(actions ?? [])];
|
||||
}, [extraActions, actions]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle title={pageTitleString} />
|
||||
@@ -140,9 +180,9 @@ export function PageDetail({
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
{actions && (
|
||||
{computedActions && (
|
||||
<Group gap={5} justify='right' wrap='nowrap' align='flex-start'>
|
||||
{actions.map((action, idx) => (
|
||||
{computedActions.map((action, idx) => (
|
||||
<Fragment key={idx}>{action}</Fragment>
|
||||
))}
|
||||
</Group>
|
||||
|
||||
@@ -30,7 +30,8 @@ export enum PluginUIFeatureType {
|
||||
panel = 'panel',
|
||||
template_editor = 'template_editor',
|
||||
template_preview = 'template_preview',
|
||||
navigation = 'navigation'
|
||||
navigation = 'navigation',
|
||||
primary_action = 'primary_action'
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -84,3 +84,11 @@ export type NavigationUIFeature = {
|
||||
featureContext: {};
|
||||
featureReturnType: undefined;
|
||||
};
|
||||
|
||||
export type PrimaryActionUIFeature = {
|
||||
featureType: 'primary_action';
|
||||
requestContext: {};
|
||||
responseOptions: PluginUIFeature;
|
||||
featureContext: {};
|
||||
featureReturnType: undefined;
|
||||
};
|
||||
|
||||
@@ -100,11 +100,8 @@ test('Stock - Location Delete', async ({ browser }) => {
|
||||
|
||||
// Delete this location, and all child locations
|
||||
await page
|
||||
.locator('div')
|
||||
.filter({
|
||||
hasText: new RegExp(`^Stock>PCB Assembler>${loc_1}Stock Location$`)
|
||||
})
|
||||
.getByLabel('action-menu-location-actions')
|
||||
.getByRole('button', { name: 'action-menu-location-actions' })
|
||||
.first()
|
||||
.click();
|
||||
await page
|
||||
.getByRole('menuitem', { name: 'action-menu-location-actions-delete' })
|
||||
|
||||
Reference in New Issue
Block a user