mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 19:46:46 +00:00
Fix for locate item plugins (#8473)
* A hook for caching active plugins * Add LocateItemButton * Implement "locate" button for location detail page too * Fix for StockApiMixin.get_serializer - Recent refactoring removed 'path_detail' attribute * Fix offloading of 'locate' * Remove debug msg * Add custom message * Remove force_async call * Add playwright tests
This commit is contained in:
parent
7b50f3b1d3
commit
246f17113f
@ -6,7 +6,7 @@ from rest_framework.generics import GenericAPIView
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from InvenTree.tasks import offload_task
|
from InvenTree.tasks import offload_task
|
||||||
from plugin.registry import registry
|
from plugin.registry import call_plugin_function, registry
|
||||||
from stock.models import StockItem, StockLocation
|
from stock.models import StockItem, StockLocation
|
||||||
|
|
||||||
|
|
||||||
@ -59,7 +59,7 @@ class LocatePluginView(GenericAPIView):
|
|||||||
StockItem.objects.get(pk=item_pk)
|
StockItem.objects.get(pk=item_pk)
|
||||||
|
|
||||||
offload_task(
|
offload_task(
|
||||||
registry.call_plugin_function,
|
call_plugin_function,
|
||||||
plugin,
|
plugin,
|
||||||
'locate_stock_item',
|
'locate_stock_item',
|
||||||
item_pk,
|
item_pk,
|
||||||
@ -78,7 +78,7 @@ class LocatePluginView(GenericAPIView):
|
|||||||
StockLocation.objects.get(pk=location_pk)
|
StockLocation.objects.get(pk=location_pk)
|
||||||
|
|
||||||
offload_task(
|
offload_task(
|
||||||
registry.call_plugin_function,
|
call_plugin_function,
|
||||||
plugin,
|
plugin,
|
||||||
'locate_stock_location',
|
'locate_stock_location',
|
||||||
location_pk,
|
location_pk,
|
||||||
|
@ -159,7 +159,7 @@ class PluginsRegistry:
|
|||||||
# Update the registry hash value
|
# Update the registry hash value
|
||||||
self.update_plugin_hash()
|
self.update_plugin_hash()
|
||||||
|
|
||||||
def call_plugin_function(self, slug, func, *args, **kwargs):
|
def call_plugin_function(self, slug: str, func: str, *args, **kwargs):
|
||||||
"""Call a member function (named by 'func') of the plugin named by 'slug'.
|
"""Call a member function (named by 'func') of the plugin named by 'slug'.
|
||||||
|
|
||||||
As this is intended to be run by the background worker,
|
As this is intended to be run by the background worker,
|
||||||
@ -807,7 +807,7 @@ class PluginsRegistry:
|
|||||||
registry: PluginsRegistry = PluginsRegistry()
|
registry: PluginsRegistry = PluginsRegistry()
|
||||||
|
|
||||||
|
|
||||||
def call_function(plugin_name, function_name, *args, **kwargs):
|
def call_plugin_function(plugin_name: str, function_name: str, *args, **kwargs):
|
||||||
"""Global helper function to call a specific member function of a plugin."""
|
"""Global helper function to call a specific member function of a plugin."""
|
||||||
return registry.call_plugin_function(plugin_name, function_name, *args, **kwargs)
|
return registry.call_plugin_function(plugin_name, function_name, *args, **kwargs)
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ from django.test import TestCase
|
|||||||
from plugin import InvenTreePlugin, registry
|
from plugin import InvenTreePlugin, registry
|
||||||
from plugin.helpers import MixinImplementationError
|
from plugin.helpers import MixinImplementationError
|
||||||
from plugin.mixins import ScheduleMixin
|
from plugin.mixins import ScheduleMixin
|
||||||
from plugin.registry import call_function
|
from plugin.registry import call_plugin_function
|
||||||
|
|
||||||
|
|
||||||
class ExampleScheduledTaskPluginTests(TestCase):
|
class ExampleScheduledTaskPluginTests(TestCase):
|
||||||
@ -67,10 +67,10 @@ class ExampleScheduledTaskPluginTests(TestCase):
|
|||||||
def test_calling(self):
|
def test_calling(self):
|
||||||
"""Check if a function can be called without errors."""
|
"""Check if a function can be called without errors."""
|
||||||
# Check with right parameters
|
# Check with right parameters
|
||||||
self.assertEqual(call_function('schedule', 'member_func'), False)
|
self.assertEqual(call_plugin_function('schedule', 'member_func'), False)
|
||||||
|
|
||||||
# Check with wrong key
|
# Check with wrong key
|
||||||
self.assertEqual(call_function('does_not_exist', 'member_func'), None)
|
self.assertEqual(call_plugin_function('does_not_exist', 'member_func'), None)
|
||||||
|
|
||||||
|
|
||||||
class ScheduledTaskPluginTests(TestCase):
|
class ScheduledTaskPluginTests(TestCase):
|
||||||
|
@ -880,6 +880,7 @@ class StockApiMixin:
|
|||||||
|
|
||||||
for key in [
|
for key in [
|
||||||
'part_detail',
|
'part_detail',
|
||||||
|
'path_detail',
|
||||||
'location_detail',
|
'location_detail',
|
||||||
'supplier_part_detail',
|
'supplier_part_detail',
|
||||||
'tests',
|
'tests',
|
||||||
|
@ -11,7 +11,7 @@ import { ActionButton } from './ActionButton';
|
|||||||
|
|
||||||
export type AdminButtonProps = {
|
export type AdminButtonProps = {
|
||||||
model: ModelType;
|
model: ModelType;
|
||||||
pk: number | undefined;
|
id: number | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -46,12 +46,12 @@ export default function AdminButton(props: Readonly<AdminButtonProps>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// No primary key provided
|
// No primary key provided
|
||||||
if (!props.pk) {
|
if (!props.id) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}, [user, props.model, props.pk]);
|
}, [user, props.model, props.id]);
|
||||||
|
|
||||||
const openAdmin = useCallback(
|
const openAdmin = useCallback(
|
||||||
(event: any) => {
|
(event: any) => {
|
||||||
@ -63,7 +63,7 @@ export default function AdminButton(props: Readonly<AdminButtonProps>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate the URL for the admin interface
|
// Generate the URL for the admin interface
|
||||||
const url = `${host}/${server.server.django_admin}${modelDef.admin_url}${props.pk}/`;
|
const url = `${host}/${server.server.django_admin}${modelDef.admin_url}${props.id}/`;
|
||||||
|
|
||||||
if (event?.ctrlKey || event?.shiftKey) {
|
if (event?.ctrlKey || event?.shiftKey) {
|
||||||
// Open the link in a new tab
|
// Open the link in a new tab
|
||||||
@ -72,7 +72,7 @@ export default function AdminButton(props: Readonly<AdminButtonProps>) {
|
|||||||
window.open(url, '_self');
|
window.open(url, '_self');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[props.model, props.pk]
|
[props.model, props.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -80,7 +80,6 @@ export default function AdminButton(props: Readonly<AdminButtonProps>) {
|
|||||||
icon={<IconUserStar />}
|
icon={<IconUserStar />}
|
||||||
color='blue'
|
color='blue'
|
||||||
size='lg'
|
size='lg'
|
||||||
radius='sm'
|
|
||||||
variant='filled'
|
variant='filled'
|
||||||
tooltip={t`Open in admin interface`}
|
tooltip={t`Open in admin interface`}
|
||||||
hidden={!enabled}
|
hidden={!enabled}
|
||||||
|
@ -72,7 +72,6 @@ export function ActionDropdown({
|
|||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<Tooltip label={tooltip} hidden={!tooltip} position='bottom'>
|
<Tooltip label={tooltip} hidden={!tooltip} position='bottom'>
|
||||||
<Button
|
<Button
|
||||||
radius='sm'
|
|
||||||
variant={noindicator ? 'transparent' : 'light'}
|
variant={noindicator ? 'transparent' : 'light'}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
aria-label={menuName}
|
aria-label={menuName}
|
||||||
|
94
src/frontend/src/components/plugins/LocateItemButton.tsx
Normal file
94
src/frontend/src/components/plugins/LocateItemButton.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { IconRadar } from '@tabler/icons-react';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
|
import { useCreateApiFormModal } from '../../hooks/UseForm';
|
||||||
|
import { usePluginsWithMixin } from '../../hooks/UsePlugins';
|
||||||
|
import { apiUrl } from '../../states/ApiState';
|
||||||
|
import { ActionButton } from '../buttons/ActionButton';
|
||||||
|
import type { ApiFormFieldSet } from '../forms/fields/ApiFormField';
|
||||||
|
import type { PluginInterface } from './PluginInterface';
|
||||||
|
|
||||||
|
export default function LocateItemButton({
|
||||||
|
stockId,
|
||||||
|
locationId
|
||||||
|
}: {
|
||||||
|
stockId?: number;
|
||||||
|
locationId?: number;
|
||||||
|
}) {
|
||||||
|
const locatePlugins = usePluginsWithMixin('locate');
|
||||||
|
|
||||||
|
const [selectedPlugin, setSelectedPlugin] = useState<string | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Ensure that the selected plugin is in the list of available plugins
|
||||||
|
if (selectedPlugin && locatePlugins) {
|
||||||
|
const plugin = locatePlugins.find(
|
||||||
|
(plugin: PluginInterface) => plugin.key === selectedPlugin
|
||||||
|
);
|
||||||
|
if (!plugin) {
|
||||||
|
setSelectedPlugin(undefined);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setSelectedPlugin(locatePlugins[0]?.key ?? undefined);
|
||||||
|
}
|
||||||
|
}, [selectedPlugin, locatePlugins]);
|
||||||
|
|
||||||
|
const locateFields: ApiFormFieldSet = useMemo(() => {
|
||||||
|
return {
|
||||||
|
plugin: {
|
||||||
|
field_type: 'choice',
|
||||||
|
value: selectedPlugin,
|
||||||
|
onValueChange: (value: string) => {
|
||||||
|
setSelectedPlugin(value);
|
||||||
|
},
|
||||||
|
choices: locatePlugins.map((plugin: PluginInterface) => {
|
||||||
|
return {
|
||||||
|
value: plugin.key,
|
||||||
|
display_name: plugin.meta?.human_name ?? plugin.name
|
||||||
|
};
|
||||||
|
})
|
||||||
|
},
|
||||||
|
item: {
|
||||||
|
hidden: true,
|
||||||
|
value: stockId
|
||||||
|
},
|
||||||
|
location: {
|
||||||
|
hidden: true,
|
||||||
|
value: locationId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [stockId, locationId, locatePlugins]);
|
||||||
|
|
||||||
|
const locateForm = useCreateApiFormModal({
|
||||||
|
url: apiUrl(ApiEndpoints.plugin_locate_item),
|
||||||
|
method: 'POST',
|
||||||
|
title: t`Locate Item`,
|
||||||
|
fields: locateFields,
|
||||||
|
successMessage: t`Item location requested`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!locatePlugins || locatePlugins.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stockId && !locationId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{locateForm.modal}
|
||||||
|
<ActionButton
|
||||||
|
icon={<IconRadar />}
|
||||||
|
variant='outline'
|
||||||
|
size='lg'
|
||||||
|
tooltip={t`Locate Item`}
|
||||||
|
onClick={locateForm.open}
|
||||||
|
tooltipAlignment='bottom'
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -203,6 +203,9 @@ export enum ApiEndpoints {
|
|||||||
// User interface plugin endpoints
|
// User interface plugin endpoints
|
||||||
plugin_ui_features_list = 'plugins/ui/features/:feature_type/',
|
plugin_ui_features_list = 'plugins/ui/features/:feature_type/',
|
||||||
|
|
||||||
|
// Special plugin endpoints
|
||||||
|
plugin_locate_item = 'locate/',
|
||||||
|
|
||||||
// Machine API endpoints
|
// Machine API endpoints
|
||||||
machine_types_list = 'machine/types/',
|
machine_types_list = 'machine/types/',
|
||||||
machine_driver_list = 'machine/drivers/',
|
machine_driver_list = 'machine/drivers/',
|
||||||
|
45
src/frontend/src/hooks/UsePlugins.tsx
Normal file
45
src/frontend/src/hooks/UsePlugins.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import type { PluginInterface } from '../components/plugins/PluginInterface';
|
||||||
|
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
||||||
|
import { useInstance } from './UseInstance';
|
||||||
|
|
||||||
|
export interface UsePluginResult {
|
||||||
|
plugins: PluginInterface[];
|
||||||
|
withMixin: (mixin: string) => PluginInterface[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for storing information on active plugins
|
||||||
|
*/
|
||||||
|
export const usePlugins = (): UsePluginResult => {
|
||||||
|
const pluginQuery = useInstance({
|
||||||
|
endpoint: ApiEndpoints.plugin_list,
|
||||||
|
defaultValue: [],
|
||||||
|
hasPrimaryKey: false,
|
||||||
|
refetchOnMount: true,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
params: {
|
||||||
|
active: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const pluginsWithMixin = useCallback(
|
||||||
|
(mixin: string) => {
|
||||||
|
return pluginQuery.instance.filter((plugin: PluginInterface) => {
|
||||||
|
return !!plugin.mixins[mixin];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[pluginQuery.instance]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
plugins: pluginQuery.instance,
|
||||||
|
withMixin: pluginsWithMixin
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePluginsWithMixin = (mixin: string): PluginInterface[] => {
|
||||||
|
const plugins = usePlugins();
|
||||||
|
|
||||||
|
return plugins.withMixin(mixin);
|
||||||
|
};
|
@ -466,7 +466,7 @@ export default function BuildDetail() {
|
|||||||
color='green'
|
color='green'
|
||||||
onClick={completeOrder.open}
|
onClick={completeOrder.open}
|
||||||
/>,
|
/>,
|
||||||
<AdminButton model={ModelType.build} pk={build.pk} />,
|
<AdminButton model={ModelType.build} id={build.pk} />,
|
||||||
<BarcodeActionDropdown
|
<BarcodeActionDropdown
|
||||||
model={ModelType.build}
|
model={ModelType.build}
|
||||||
pk={build.pk}
|
pk={build.pk}
|
||||||
|
@ -287,7 +287,7 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
|
|||||||
|
|
||||||
const companyActions = useMemo(() => {
|
const companyActions = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
<AdminButton model={ModelType.company} pk={company.pk} />,
|
<AdminButton model={ModelType.company} id={company.pk} />,
|
||||||
<OptionsActionDropdown
|
<OptionsActionDropdown
|
||||||
tooltip={t`Company Actions`}
|
tooltip={t`Company Actions`}
|
||||||
actions={[
|
actions={[
|
||||||
|
@ -234,7 +234,7 @@ export default function ManufacturerPartDetail() {
|
|||||||
<AdminButton
|
<AdminButton
|
||||||
key='admin'
|
key='admin'
|
||||||
model={ModelType.manufacturerpart}
|
model={ModelType.manufacturerpart}
|
||||||
pk={manufacturerPart.pk}
|
id={manufacturerPart.pk}
|
||||||
/>,
|
/>,
|
||||||
<OptionsActionDropdown
|
<OptionsActionDropdown
|
||||||
key='options'
|
key='options'
|
||||||
|
@ -276,7 +276,7 @@ export default function SupplierPartDetail() {
|
|||||||
|
|
||||||
const supplierPartActions = useMemo(() => {
|
const supplierPartActions = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
<AdminButton model={ModelType.supplierpart} pk={supplierPart.pk} />,
|
<AdminButton model={ModelType.supplierpart} id={supplierPart.pk} />,
|
||||||
<BarcodeActionDropdown
|
<BarcodeActionDropdown
|
||||||
model={ModelType.supplierpart}
|
model={ModelType.supplierpart}
|
||||||
pk={supplierPart.pk}
|
pk={supplierPart.pk}
|
||||||
|
@ -223,7 +223,7 @@ export default function CategoryDetail() {
|
|||||||
<AdminButton
|
<AdminButton
|
||||||
key='admin'
|
key='admin'
|
||||||
model={ModelType.partcategory}
|
model={ModelType.partcategory}
|
||||||
pk={category.pk}
|
id={category.pk}
|
||||||
/>,
|
/>,
|
||||||
<OptionsActionDropdown
|
<OptionsActionDropdown
|
||||||
key='category-actions'
|
key='category-actions'
|
||||||
|
@ -996,7 +996,7 @@ export default function PartDetail() {
|
|||||||
|
|
||||||
const partActions = useMemo(() => {
|
const partActions = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
<AdminButton model={ModelType.part} pk={part.pk} />,
|
<AdminButton model={ModelType.part} id={part.pk} />,
|
||||||
<BarcodeActionDropdown
|
<BarcodeActionDropdown
|
||||||
model={ModelType.part}
|
model={ModelType.part}
|
||||||
pk={part.pk}
|
pk={part.pk}
|
||||||
|
@ -413,7 +413,7 @@ export default function PurchaseOrderDetail() {
|
|||||||
color='green'
|
color='green'
|
||||||
onClick={completeOrder.open}
|
onClick={completeOrder.open}
|
||||||
/>,
|
/>,
|
||||||
<AdminButton model={ModelType.purchaseorder} pk={order.pk} />,
|
<AdminButton model={ModelType.purchaseorder} id={order.pk} />,
|
||||||
<BarcodeActionDropdown
|
<BarcodeActionDropdown
|
||||||
model={ModelType.purchaseorder}
|
model={ModelType.purchaseorder}
|
||||||
pk={order.pk}
|
pk={order.pk}
|
||||||
|
@ -412,7 +412,7 @@ export default function ReturnOrderDetail() {
|
|||||||
color='green'
|
color='green'
|
||||||
onClick={() => completeOrder.open()}
|
onClick={() => completeOrder.open()}
|
||||||
/>,
|
/>,
|
||||||
<AdminButton model={ModelType.returnorder} pk={order.pk} />,
|
<AdminButton model={ModelType.returnorder} id={order.pk} />,
|
||||||
<BarcodeActionDropdown
|
<BarcodeActionDropdown
|
||||||
model={ModelType.returnorder}
|
model={ModelType.returnorder}
|
||||||
pk={order.pk}
|
pk={order.pk}
|
||||||
|
@ -453,7 +453,7 @@ export default function SalesOrderDetail() {
|
|||||||
color='green'
|
color='green'
|
||||||
onClick={completeOrder.open}
|
onClick={completeOrder.open}
|
||||||
/>,
|
/>,
|
||||||
<AdminButton model={ModelType.salesorder} pk={order.pk} />,
|
<AdminButton model={ModelType.salesorder} id={order.pk} />,
|
||||||
<BarcodeActionDropdown
|
<BarcodeActionDropdown
|
||||||
model={ModelType.salesorder}
|
model={ModelType.salesorder}
|
||||||
pk={order.pk}
|
pk={order.pk}
|
||||||
|
@ -25,6 +25,7 @@ import NavigationTree from '../../components/nav/NavigationTree';
|
|||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
import type { PanelType } from '../../components/panels/Panel';
|
import type { PanelType } from '../../components/panels/Panel';
|
||||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||||
|
import LocateItemButton from '../../components/plugins/LocateItemButton';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { ModelType } from '../../enums/ModelType';
|
import { ModelType } from '../../enums/ModelType';
|
||||||
import { UserRoles } from '../../enums/Roles';
|
import { UserRoles } from '../../enums/Roles';
|
||||||
@ -275,7 +276,8 @@ export default function Stock() {
|
|||||||
|
|
||||||
const locationActions = useMemo(
|
const locationActions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
<AdminButton model={ModelType.stocklocation} pk={location.pk} />,
|
<AdminButton model={ModelType.stocklocation} id={location.pk} />,
|
||||||
|
<LocateItemButton locationId={location.pk} />,
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon={<InvenTreeIcon icon='stocktake' />}
|
icon={<InvenTreeIcon icon='stocktake' />}
|
||||||
onClick={notYetImplemented}
|
onClick={notYetImplemented}
|
||||||
|
@ -39,6 +39,7 @@ import AttachmentPanel from '../../components/panels/AttachmentPanel';
|
|||||||
import NotesPanel from '../../components/panels/NotesPanel';
|
import NotesPanel from '../../components/panels/NotesPanel';
|
||||||
import type { PanelType } from '../../components/panels/Panel';
|
import type { PanelType } from '../../components/panels/Panel';
|
||||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||||
|
import LocateItemButton from '../../components/plugins/LocateItemButton';
|
||||||
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
||||||
import { formatCurrency } from '../../defaults/formatters';
|
import { formatCurrency } from '../../defaults/formatters';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
@ -653,7 +654,8 @@ export default function StockDetail() {
|
|||||||
stockitem.quantity == 1;
|
stockitem.quantity == 1;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
<AdminButton model={ModelType.stockitem} pk={stockitem.pk} />,
|
<AdminButton model={ModelType.stockitem} id={stockitem.pk} />,
|
||||||
|
<LocateItemButton stockId={stockitem.pk} />,
|
||||||
<BarcodeActionDropdown
|
<BarcodeActionDropdown
|
||||||
model={ModelType.stockitem}
|
model={ModelType.stockitem}
|
||||||
pk={stockitem.pk}
|
pk={stockitem.pk}
|
||||||
|
@ -115,7 +115,7 @@ export function GroupDrawer({
|
|||||||
<Title order={5}>
|
<Title order={5}>
|
||||||
<Trans>Permission set</Trans>
|
<Trans>Permission set</Trans>
|
||||||
</Title>
|
</Title>
|
||||||
<AdminButton model={ModelType.group} pk={instance.pk} />
|
<AdminButton model={ModelType.group} id={instance.pk} />
|
||||||
</Group>
|
</Group>
|
||||||
<Group>{permissionsAccordion}</Group>
|
<Group>{permissionsAccordion}</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -94,3 +94,35 @@ test('Plugins - Custom Admin', async ({ page, request }) => {
|
|||||||
await page.getByText('foo: bar').waitFor();
|
await page.getByText('foo: bar').waitFor();
|
||||||
await page.getByText('hello: world').waitFor();
|
await page.getByText('hello: world').waitFor();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Plugins - Locate Item', async ({ page, request }) => {
|
||||||
|
await doQuickLogin(page, 'admin', 'inventree');
|
||||||
|
|
||||||
|
// Ensure that the sample location plugin is enabled
|
||||||
|
await setPluginState({
|
||||||
|
request,
|
||||||
|
plugin: 'samplelocate',
|
||||||
|
state: true
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Navigate to the "stock item" page
|
||||||
|
await page.goto(`${baseUrl}/stock/item/287/`);
|
||||||
|
|
||||||
|
// "Locate" this item
|
||||||
|
await page.getByLabel('action-button-locate-item').click();
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
await page.getByText('Item location requested').waitFor();
|
||||||
|
|
||||||
|
// Show the location
|
||||||
|
await page.getByLabel('breadcrumb-1-factory').click();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
await page.getByLabel('action-button-locate-item').click();
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
await page.getByText('Item location requested').waitFor();
|
||||||
|
|
||||||
|
await page.waitForTimeout(2500);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user