mirror of
https://github.com/inventree/InvenTree.git
synced 2026-09-02 10:18:42 +00:00
Merge commit 'd38af61ba2fb3c87143da57ddd92bc4845f85a7c' into block-notes
This commit is contained in:
@@ -726,6 +726,11 @@ jobs:
|
|||||||
- name: Install Playwright OS dependencies
|
- name: Install Playwright OS dependencies
|
||||||
if: steps.playwright-cache.outputs.cache-hit == 'true'
|
if: steps.playwright-cache.outputs.cache-hit == 'true'
|
||||||
run: cd src/frontend && npx playwright install-deps
|
run: cd src/frontend && npx playwright install-deps
|
||||||
|
- name: Install Sample Plugin
|
||||||
|
run: |
|
||||||
|
pip install -U inventree-plugin-creator
|
||||||
|
create-inventree-plugin --default
|
||||||
|
cd MyCustomPlugin && pip install -e . && cd frontend && npm install && npm run translate && npm run build
|
||||||
- name: Run Playwright tests
|
- name: Run Playwright tests
|
||||||
id: tests
|
id: tests
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 498
|
INVENTREE_API_VERSION = 499
|
||||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
INVENTREE_API_TEXT = """
|
||||||
|
|
||||||
|
v499 -> 2026-06-01 : https://github.com/inventree/InvenTree/pull/12057
|
||||||
|
- Fixes search field issues on the BarcodeScanHistory API endpoint
|
||||||
|
|
||||||
v498 -> 2026-05-31 : https://github.com/inventree/InvenTree/pull/12055
|
v498 -> 2026-05-31 : https://github.com/inventree/InvenTree/pull/12055
|
||||||
- Updates the "status_text" field for models which support custom status values
|
- Updates the "status_text" field for models which support custom status values
|
||||||
|
|
||||||
|
|||||||
@@ -3539,6 +3539,11 @@ class EmailThread(InvenTree.models.InvenTreeMetadataModel):
|
|||||||
unique_together = [['key', 'global_id']]
|
unique_together = [['key', 'global_id']]
|
||||||
ordering = ['-updated']
|
ordering = ['-updated']
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_api_url():
|
||||||
|
"""Return the API URL associated with the EmailThread model."""
|
||||||
|
return reverse('api-email-list')
|
||||||
|
|
||||||
key = models.CharField(
|
key = models.CharField(
|
||||||
max_length=250,
|
max_length=250,
|
||||||
verbose_name=_('Key'),
|
verbose_name=_('Key'),
|
||||||
|
|||||||
@@ -121,3 +121,7 @@ class TransitionTests(InvenTreeTestCase):
|
|||||||
self.assertIn(
|
self.assertIn(
|
||||||
"ValueError('This is a broken transition plugin!')", str(cm.output[0])
|
"ValueError('This is a broken transition plugin!')", str(cm.output[0])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Ensure the plugin is now disabled
|
||||||
|
registry.set_plugin_state('sample-transition', False)
|
||||||
|
registry.set_plugin_state('sample-broken-transition', False)
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class TransitionMethod:
|
|||||||
**kwargs: Additional keyword arguments for custom logic.
|
**kwargs: Additional keyword arguments for custom logic.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
result: bool - True if the transition method was successful, False otherwise.
|
result: bool - True if the transition method was successful (and no further transitions are attempted), False otherwise.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValidationError: Alert the user that the transition failed
|
ValidationError: Alert the user that the transition failed
|
||||||
|
|||||||
@@ -853,11 +853,11 @@ class BarcodeScanResultList(BarcodeScanResultMixin, BulkDeleteMixin, ListAPI):
|
|||||||
filterset_class = BarcodeScanResultFilter
|
filterset_class = BarcodeScanResultFilter
|
||||||
filter_backends = SEARCH_ORDER_FILTER
|
filter_backends = SEARCH_ORDER_FILTER
|
||||||
|
|
||||||
ordering_fields = ['user', 'plugin', 'timestamp', 'endpoint', 'result']
|
ordering_fields = ['user', 'timestamp', 'endpoint', 'result']
|
||||||
|
|
||||||
ordering = '-timestamp'
|
ordering = '-timestamp'
|
||||||
|
|
||||||
search_fields = ['plugin']
|
search_fields = ['data']
|
||||||
|
|
||||||
|
|
||||||
class BarcodeScanResultDetail(BarcodeScanResultMixin, RetrieveDestroyAPI):
|
class BarcodeScanResultDetail(BarcodeScanResultMixin, RetrieveDestroyAPI):
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ class AppMixin:
|
|||||||
"""
|
"""
|
||||||
from common.settings import get_global_setting
|
from common.settings import get_global_setting
|
||||||
|
|
||||||
if settings.PLUGIN_TESTING or get_global_setting('ENABLE_PLUGINS_APP'):
|
if settings.PLUGIN_TESTING or get_global_setting(
|
||||||
|
'ENABLE_PLUGINS_APP', create=False
|
||||||
|
):
|
||||||
logger.info('Registering IntegrationPlugin apps')
|
logger.info('Registering IntegrationPlugin apps')
|
||||||
apps_changed = False
|
apps_changed = False
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { DefaultMantineColor, MantineStyleProp } from '@mantine/core';
|
|||||||
import type { UseFormReturnType } from '@mantine/form';
|
import type { UseFormReturnType } from '@mantine/form';
|
||||||
import type { JSX, ReactNode } from 'react';
|
import type { JSX, ReactNode } from 'react';
|
||||||
import type { FieldValues, UseFormReturn } from 'react-hook-form';
|
import type { FieldValues, UseFormReturn } from 'react-hook-form';
|
||||||
|
import type { NavigateFunction } from 'react-router-dom';
|
||||||
import type { ApiEndpoints } from '../enums/ApiEndpoints';
|
import type { ApiEndpoints } from '../enums/ApiEndpoints';
|
||||||
import type { ModelType } from '../enums/ModelType';
|
import type { ModelType } from '../enums/ModelType';
|
||||||
import type { PathParams, UiSizeType } from './Core';
|
import type { PathParams, UiSizeType } from './Core';
|
||||||
@@ -160,6 +161,7 @@ export type ApiFormFieldSet = Record<string, ApiFormFieldType>;
|
|||||||
* @param modelType : Define a model type for this form
|
* @param modelType : Define a model type for this form
|
||||||
* @param follow : Boolean, follow the result of the form (if possible)
|
* @param follow : Boolean, follow the result of the form (if possible)
|
||||||
* @param table : Table to update on success (if provided)
|
* @param table : Table to update on success (if provided)
|
||||||
|
* @param navigate : Optional navigate function to use for following results (if follow is true)
|
||||||
*/
|
*/
|
||||||
export interface ApiFormProps {
|
export interface ApiFormProps {
|
||||||
url: ApiEndpoints | string;
|
url: ApiEndpoints | string;
|
||||||
@@ -189,6 +191,7 @@ export interface ApiFormProps {
|
|||||||
follow?: boolean;
|
follow?: boolean;
|
||||||
actions?: ApiFormAction[];
|
actions?: ApiFormAction[];
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
navigate?: NavigateFunction;
|
||||||
keepOpenOption?: boolean;
|
keepOpenOption?: boolean;
|
||||||
onKeepOpenChange?: (keepOpen: boolean) => void;
|
onKeepOpenChange?: (keepOpen: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { IconX } from '@tabler/icons-react';
|
|||||||
import { Boundary } from '@lib/components/Boundary';
|
import { Boundary } from '@lib/components/Boundary';
|
||||||
|
|
||||||
import type { ModelType } from '@lib/index';
|
import type { ModelType } from '@lib/index';
|
||||||
|
import type { InvenTreeIconType } from '@lib/types/Icons';
|
||||||
import type { JSX } from 'react';
|
import type { JSX } from 'react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -22,6 +23,7 @@ export interface DashboardWidgetProps {
|
|||||||
minWidth?: number;
|
minWidth?: number;
|
||||||
minHeight?: number;
|
minHeight?: number;
|
||||||
modelType?: ModelType;
|
modelType?: ModelType;
|
||||||
|
icon?: keyof InvenTreeIconType;
|
||||||
render: () => JSX.Element;
|
render: () => JSX.Element;
|
||||||
visible?: () => boolean;
|
visible?: () => boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ import {
|
|||||||
Tooltip
|
Tooltip
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useDebouncedValue } from '@mantine/hooks';
|
import { useDebouncedValue } from '@mantine/hooks';
|
||||||
import { IconBackspace, IconLayoutGridAdd } from '@tabler/icons-react';
|
import { IconBackspace } from '@tabler/icons-react';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { StylishText } from '@lib/components/StylishText';
|
import { StylishText } from '@lib/components/StylishText';
|
||||||
|
import { InvenTreeIcon } from '../../functions/icons';
|
||||||
import { useDashboardItems } from '../../hooks/UseDashboardItems';
|
import { useDashboardItems } from '../../hooks/UseDashboardItems';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -105,7 +106,7 @@ export default function DashboardWidgetDrawer({
|
|||||||
onAddWidget(widget.label);
|
onAddWidget(widget.label);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconLayoutGridAdd />
|
<InvenTreeIcon icon={widget.icon ?? 'dashboard'} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import ColorToggleDashboardWidget from './widgets/ColorToggleWidget';
|
|||||||
import GetStartedWidget from './widgets/GetStartedWidget';
|
import GetStartedWidget from './widgets/GetStartedWidget';
|
||||||
import LanguageSelectDashboardWidget from './widgets/LanguageSelectWidget';
|
import LanguageSelectDashboardWidget from './widgets/LanguageSelectWidget';
|
||||||
import NewsWidget from './widgets/NewsWidget';
|
import NewsWidget from './widgets/NewsWidget';
|
||||||
|
import OrderHistoryWidget from './widgets/OrderHistoryWidget';
|
||||||
import QueryCountDashboardWidget from './widgets/QueryCountDashboardWidget';
|
import QueryCountDashboardWidget from './widgets/QueryCountDashboardWidget';
|
||||||
import QueryDashboardWidget from './widgets/QueryDashboardWidget';
|
import QueryDashboardWidget from './widgets/QueryDashboardWidget';
|
||||||
import StocktakeDashboardWidget from './widgets/StocktakeDashboardWidget';
|
import StocktakeDashboardWidget from './widgets/StocktakeDashboardWidget';
|
||||||
@@ -43,6 +44,7 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] {
|
|||||||
title: t`Invalid BOMs`,
|
title: t`Invalid BOMs`,
|
||||||
description: t`Assemblies requiring bill of materials validation`,
|
description: t`Assemblies requiring bill of materials validation`,
|
||||||
modelType: ModelType.part,
|
modelType: ModelType.part,
|
||||||
|
icon: 'exclamation',
|
||||||
params: {
|
params: {
|
||||||
active: true, // Only show active parts
|
active: true, // Only show active parts
|
||||||
assembly: true, // Only show parts which are assemblies
|
assembly: true, // Only show parts which are assemblies
|
||||||
@@ -94,7 +96,8 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] {
|
|||||||
description: t`Show the number of stock items which have expired`,
|
description: t`Show the number of stock items which have expired`,
|
||||||
modelType: ModelType.stockitem,
|
modelType: ModelType.stockitem,
|
||||||
params: { expired: true },
|
params: { expired: true },
|
||||||
enabled: globalSettings.isSet('STOCK_ENABLE_EXPIRY')
|
enabled: globalSettings.isSet('STOCK_ENABLE_EXPIRY'),
|
||||||
|
icon: 'overdue'
|
||||||
}),
|
}),
|
||||||
QueryCountDashboardWidget({
|
QueryCountDashboardWidget({
|
||||||
title: t`Stale Stock Items`,
|
title: t`Stale Stock Items`,
|
||||||
@@ -116,14 +119,16 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] {
|
|||||||
label: 'ovr-bo',
|
label: 'ovr-bo',
|
||||||
description: t`Show the number of build orders which are overdue`,
|
description: t`Show the number of build orders which are overdue`,
|
||||||
modelType: ModelType.build,
|
modelType: ModelType.build,
|
||||||
params: { overdue: true }
|
params: { overdue: true },
|
||||||
|
icon: 'overdue'
|
||||||
}),
|
}),
|
||||||
QueryCountDashboardWidget({
|
QueryCountDashboardWidget({
|
||||||
title: t`Assigned Build Orders`,
|
title: t`Assigned Build Orders`,
|
||||||
label: 'asn-bo',
|
label: 'asn-bo',
|
||||||
description: t`Show the number of build orders which are assigned to you`,
|
description: t`Show the number of build orders which are assigned to you`,
|
||||||
modelType: ModelType.build,
|
modelType: ModelType.build,
|
||||||
params: { assigned_to_me: true, outstanding: true }
|
params: { assigned_to_me: true, outstanding: true },
|
||||||
|
icon: 'responsible'
|
||||||
}),
|
}),
|
||||||
QueryCountDashboardWidget({
|
QueryCountDashboardWidget({
|
||||||
title: t`Active Sales Orders`,
|
title: t`Active Sales Orders`,
|
||||||
@@ -137,14 +142,16 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] {
|
|||||||
label: 'ovr-so',
|
label: 'ovr-so',
|
||||||
description: t`Show the number of sales orders which are overdue`,
|
description: t`Show the number of sales orders which are overdue`,
|
||||||
modelType: ModelType.salesorder,
|
modelType: ModelType.salesorder,
|
||||||
params: { overdue: true }
|
params: { overdue: true },
|
||||||
|
icon: 'overdue'
|
||||||
}),
|
}),
|
||||||
QueryCountDashboardWidget({
|
QueryCountDashboardWidget({
|
||||||
title: t`Assigned Sales Orders`,
|
title: t`Assigned Sales Orders`,
|
||||||
label: 'asn-so',
|
label: 'asn-so',
|
||||||
description: t`Show the number of sales orders which are assigned to you`,
|
description: t`Show the number of sales orders which are assigned to you`,
|
||||||
modelType: ModelType.salesorder,
|
modelType: ModelType.salesorder,
|
||||||
params: { assigned_to_me: true, outstanding: true }
|
params: { assigned_to_me: true, outstanding: true },
|
||||||
|
icon: 'responsible'
|
||||||
}),
|
}),
|
||||||
QueryCountDashboardWidget({
|
QueryCountDashboardWidget({
|
||||||
title: t`Pending Shipments`,
|
title: t`Pending Shipments`,
|
||||||
@@ -165,14 +172,16 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] {
|
|||||||
label: 'ovr-po',
|
label: 'ovr-po',
|
||||||
description: t`Show the number of purchase orders which are overdue`,
|
description: t`Show the number of purchase orders which are overdue`,
|
||||||
modelType: ModelType.purchaseorder,
|
modelType: ModelType.purchaseorder,
|
||||||
params: { overdue: true }
|
params: { overdue: true },
|
||||||
|
icon: 'overdue'
|
||||||
}),
|
}),
|
||||||
QueryCountDashboardWidget({
|
QueryCountDashboardWidget({
|
||||||
title: t`Assigned Purchase Orders`,
|
title: t`Assigned Purchase Orders`,
|
||||||
label: 'asn-po',
|
label: 'asn-po',
|
||||||
description: t`Show the number of purchase orders which are assigned to you`,
|
description: t`Show the number of purchase orders which are assigned to you`,
|
||||||
modelType: ModelType.purchaseorder,
|
modelType: ModelType.purchaseorder,
|
||||||
params: { assigned_to_me: true, outstanding: true }
|
params: { assigned_to_me: true, outstanding: true },
|
||||||
|
icon: 'responsible'
|
||||||
}),
|
}),
|
||||||
QueryCountDashboardWidget({
|
QueryCountDashboardWidget({
|
||||||
title: t`Active Return Orders`,
|
title: t`Active Return Orders`,
|
||||||
@@ -186,14 +195,16 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] {
|
|||||||
label: 'ovr-ro',
|
label: 'ovr-ro',
|
||||||
description: t`Show the number of return orders which are overdue`,
|
description: t`Show the number of return orders which are overdue`,
|
||||||
modelType: ModelType.returnorder,
|
modelType: ModelType.returnorder,
|
||||||
params: { overdue: true }
|
params: { overdue: true },
|
||||||
|
icon: 'overdue'
|
||||||
}),
|
}),
|
||||||
QueryCountDashboardWidget({
|
QueryCountDashboardWidget({
|
||||||
title: t`Assigned Return Orders`,
|
title: t`Assigned Return Orders`,
|
||||||
label: 'asn-ro',
|
label: 'asn-ro',
|
||||||
description: t`Show the number of return orders which are assigned to you`,
|
description: t`Show the number of return orders which are assigned to you`,
|
||||||
modelType: ModelType.returnorder,
|
modelType: ModelType.returnorder,
|
||||||
params: { assigned_to_me: true, outstanding: true }
|
params: { assigned_to_me: true, outstanding: true },
|
||||||
|
icon: 'responsible'
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -207,6 +218,26 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function BuiltinHistoryWidgets(): DashboardWidgetProps[] {
|
||||||
|
return [
|
||||||
|
OrderHistoryWidget({
|
||||||
|
modelType: ModelType.build
|
||||||
|
}),
|
||||||
|
OrderHistoryWidget({
|
||||||
|
modelType: ModelType.salesorder
|
||||||
|
}),
|
||||||
|
OrderHistoryWidget({
|
||||||
|
modelType: ModelType.purchaseorder
|
||||||
|
}),
|
||||||
|
OrderHistoryWidget({
|
||||||
|
modelType: ModelType.returnorder
|
||||||
|
}),
|
||||||
|
OrderHistoryWidget({
|
||||||
|
modelType: ModelType.transferorder
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function BuiltinGettingStartedWidgets(): DashboardWidgetProps[] {
|
function BuiltinGettingStartedWidgets(): DashboardWidgetProps[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -215,6 +246,7 @@ function BuiltinGettingStartedWidgets(): DashboardWidgetProps[] {
|
|||||||
description: t`Getting started with InvenTree`,
|
description: t`Getting started with InvenTree`,
|
||||||
minWidth: 5,
|
minWidth: 5,
|
||||||
minHeight: 4,
|
minHeight: 4,
|
||||||
|
icon: 'info',
|
||||||
render: () => <GetStartedWidget />
|
render: () => <GetStartedWidget />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -223,6 +255,7 @@ function BuiltinGettingStartedWidgets(): DashboardWidgetProps[] {
|
|||||||
description: t`The latest news from InvenTree`,
|
description: t`The latest news from InvenTree`,
|
||||||
minWidth: 5,
|
minWidth: 5,
|
||||||
minHeight: 4,
|
minHeight: 4,
|
||||||
|
icon: 'news',
|
||||||
render: () => <NewsWidget />
|
render: () => <NewsWidget />
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -243,6 +276,7 @@ function BuiltinActionWidgets(): DashboardWidgetProps[] {
|
|||||||
export default function DashboardWidgetLibrary(): DashboardWidgetProps[] {
|
export default function DashboardWidgetLibrary(): DashboardWidgetProps[] {
|
||||||
return [
|
return [
|
||||||
...BuiltinQueryCountWidgets(),
|
...BuiltinQueryCountWidgets(),
|
||||||
|
...BuiltinHistoryWidgets(),
|
||||||
...BuiltinGettingStartedWidgets(),
|
...BuiltinGettingStartedWidgets(),
|
||||||
...BuiltinSettingsWidgets(),
|
...BuiltinSettingsWidgets(),
|
||||||
...BuiltinActionWidgets()
|
...BuiltinActionWidgets()
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { ModelInformationDict } from '@lib/enums/ModelInformation';
|
||||||
|
import type { ModelType } from '@lib/enums/ModelType';
|
||||||
|
import { apiUrl } from '@lib/functions/Api';
|
||||||
|
import { StylishText } from '@lib/index';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
|
import { BarChart } from '@mantine/charts';
|
||||||
|
import { Stack } from '@mantine/core';
|
||||||
|
import { useDocumentVisibility } from '@mantine/hooks';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useApi } from '../../../contexts/ApiContext';
|
||||||
|
import { useUserState } from '../../../states/UserState';
|
||||||
|
import type { DashboardWidgetProps } from '../DashboardWidget';
|
||||||
|
|
||||||
|
function OrderHistoryComponent({
|
||||||
|
modelType,
|
||||||
|
title
|
||||||
|
}: {
|
||||||
|
modelType: ModelType;
|
||||||
|
title: string;
|
||||||
|
}) {
|
||||||
|
const modelInfo = useMemo(() => {
|
||||||
|
return ModelInformationDict[modelType];
|
||||||
|
}, [modelType]);
|
||||||
|
|
||||||
|
const url = useMemo(() => {
|
||||||
|
return apiUrl(modelInfo.api_endpoint);
|
||||||
|
}, [modelInfo]);
|
||||||
|
|
||||||
|
const api = useApi();
|
||||||
|
const visibility = useDocumentVisibility();
|
||||||
|
|
||||||
|
const endDate = dayjs().add(1, 'day').format('YYYY-MM-DD');
|
||||||
|
const startDate = dayjs()
|
||||||
|
.subtract(12, 'month')
|
||||||
|
.subtract(1, 'day')
|
||||||
|
.format('YYYY-MM-DD');
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
completed_after: startDate,
|
||||||
|
completed_before: endDate
|
||||||
|
};
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: ['dashboard-order-summary', modelType, params, visibility],
|
||||||
|
enabled: visibility === 'visible',
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
refetchOnMount: true,
|
||||||
|
refetchInterval: 10 * 60 * 1000, // 10 minute refetch interval
|
||||||
|
staleTime: 5 * 60 * 1000, // 5 minute stale time
|
||||||
|
queryFn: () => {
|
||||||
|
if (visibility !== 'visible') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.get(url, { params }).then((res) => {
|
||||||
|
return res.data ?? [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const chartData = useMemo(() => {
|
||||||
|
const months = Array.from({ length: 13 }, (_, i) => ({
|
||||||
|
month: dayjs()
|
||||||
|
.subtract(12 - i, 'month')
|
||||||
|
.format('MMM YY'),
|
||||||
|
count: 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (const order of query.data || []) {
|
||||||
|
// Build orders use `completion_date`; all other order types use `complete_date`
|
||||||
|
const dateStr =
|
||||||
|
order.complete_date ?? order.completion_date ?? order.shipment_date;
|
||||||
|
if (!dateStr) continue;
|
||||||
|
const label = dayjs(dateStr).format('MMM YY');
|
||||||
|
const entry = months.find((m) => m.month === label);
|
||||||
|
if (entry) entry.count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return months;
|
||||||
|
}, [query.data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap='xs'>
|
||||||
|
<StylishText size='md'>{title}</StylishText>
|
||||||
|
<BarChart
|
||||||
|
h={200}
|
||||||
|
data={chartData}
|
||||||
|
dataKey='month'
|
||||||
|
series={[{ name: 'count', label: t`Completed`, color: 'blue.6' }]}
|
||||||
|
withYAxis={false}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a simple chart of the number of completed orders per month, for the last 12 months.
|
||||||
|
*/
|
||||||
|
export default function OrderHistoryWidget({
|
||||||
|
modelType
|
||||||
|
}: {
|
||||||
|
modelType: ModelType;
|
||||||
|
}): DashboardWidgetProps {
|
||||||
|
const user = useUserState();
|
||||||
|
|
||||||
|
const modelInfo = useMemo(() => {
|
||||||
|
return ModelInformationDict[modelType];
|
||||||
|
}, [modelType]);
|
||||||
|
|
||||||
|
// Extract translated model labels
|
||||||
|
const models = modelInfo.label_multiple();
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: `${modelType}-history`,
|
||||||
|
title: t`Completed ${models}`,
|
||||||
|
description: t`Display number of completed ${models} per month`,
|
||||||
|
minHeight: 2,
|
||||||
|
minWidth: 3,
|
||||||
|
modelType: modelType,
|
||||||
|
icon: 'chart_bar',
|
||||||
|
visible: () => user.hasViewPermission(modelType),
|
||||||
|
render: () => (
|
||||||
|
<OrderHistoryComponent
|
||||||
|
modelType={modelType}
|
||||||
|
title={t`Completed ${models}`}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -129,6 +129,7 @@ export default function QueryCountDashboardWidget({
|
|||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
modelType,
|
modelType,
|
||||||
|
icon,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
params
|
params
|
||||||
}: {
|
}: {
|
||||||
@@ -136,6 +137,7 @@ export default function QueryCountDashboardWidget({
|
|||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
modelType: ModelType;
|
modelType: ModelType;
|
||||||
|
icon?: keyof InvenTreeIconType;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
params: any;
|
params: any;
|
||||||
}): DashboardWidgetProps {
|
}): DashboardWidgetProps {
|
||||||
@@ -147,6 +149,7 @@ export default function QueryCountDashboardWidget({
|
|||||||
modelType: modelType,
|
modelType: modelType,
|
||||||
minWidth: 2,
|
minWidth: 2,
|
||||||
minHeight: 1,
|
minHeight: 1,
|
||||||
|
icon: icon,
|
||||||
render: () => (
|
render: () => (
|
||||||
<QueryCountWidget modelType={modelType} title={title} params={params} />
|
<QueryCountWidget modelType={modelType} title={title} params={params} />
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export default function StocktakeDashboardWidget(): DashboardWidgetProps {
|
|||||||
minHeight: 1,
|
minHeight: 1,
|
||||||
minWidth: 2,
|
minWidth: 2,
|
||||||
render: () => <StocktakeWidget />,
|
render: () => <StocktakeWidget />,
|
||||||
|
icon: 'stocktake',
|
||||||
enabled: user.hasAddRole(UserRoles.part)
|
enabled: user.hasAddRole(UserRoles.part)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
type SubmitHandler,
|
type SubmitHandler,
|
||||||
useForm
|
useForm
|
||||||
} from 'react-hook-form';
|
} from 'react-hook-form';
|
||||||
import { type NavigateFunction, useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { Boundary } from '@lib/components/Boundary';
|
import { Boundary } from '@lib/components/Boundary';
|
||||||
import { isTrue } from '@lib/functions/Conversion';
|
import { isTrue } from '@lib/functions/Conversion';
|
||||||
@@ -176,14 +176,15 @@ export function ApiForm({
|
|||||||
props.onKeepOpenChange?.(v);
|
props.onKeepOpenChange?.(v);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Accessor for the navigation function (which is used to redirect the user)
|
let navigate = props.navigate || null;
|
||||||
let navigate: NavigateFunction | null = null;
|
|
||||||
|
|
||||||
try {
|
if (!navigate) {
|
||||||
navigate = useNavigate();
|
try {
|
||||||
} catch (_error) {
|
navigate = useNavigate();
|
||||||
// Note: If we launch a form within a plugin context, useNavigate() may not be available
|
} catch (_error) {
|
||||||
navigate = null;
|
// Note: If we launch a form within a plugin context, useNavigate() may not be available
|
||||||
|
navigate = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [fields, setFields] = useState<ApiFormFieldSet>(
|
const [fields, setFields] = useState<ApiFormFieldSet>(
|
||||||
@@ -658,6 +659,7 @@ export function ApiForm({
|
|||||||
definition={field}
|
definition={field}
|
||||||
control={form.control}
|
control={form.control}
|
||||||
url={url}
|
url={url}
|
||||||
|
navigate={navigate}
|
||||||
setFields={setFields}
|
setFields={setFields}
|
||||||
onKeyDown={(value) => {
|
onKeyDown={(value) => {
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { type Control, type FieldValues, useController } from 'react-hook-form';
|
|||||||
|
|
||||||
import type { ApiFormFieldSet, ApiFormFieldType } from '@lib/types/Forms';
|
import type { ApiFormFieldSet, ApiFormFieldType } from '@lib/types/Forms';
|
||||||
import { IconFileUpload } from '@tabler/icons-react';
|
import { IconFileUpload } from '@tabler/icons-react';
|
||||||
|
import type { NavigateFunction } from 'react-router-dom';
|
||||||
import DateTimeField from '../DateTimeField';
|
import DateTimeField from '../DateTimeField';
|
||||||
import { BooleanField } from './BooleanField';
|
import { BooleanField } from './BooleanField';
|
||||||
import { ChoiceField } from './ChoiceField';
|
import { ChoiceField } from './ChoiceField';
|
||||||
@@ -26,6 +27,7 @@ export function ApiFormField({
|
|||||||
definition,
|
definition,
|
||||||
control,
|
control,
|
||||||
hideLabels,
|
hideLabels,
|
||||||
|
navigate,
|
||||||
url,
|
url,
|
||||||
setFields,
|
setFields,
|
||||||
onKeyDown
|
onKeyDown
|
||||||
@@ -34,6 +36,7 @@ export function ApiFormField({
|
|||||||
definition: ApiFormFieldType;
|
definition: ApiFormFieldType;
|
||||||
control: Control<FieldValues, any>;
|
control: Control<FieldValues, any>;
|
||||||
hideLabels?: boolean;
|
hideLabels?: boolean;
|
||||||
|
navigate?: NavigateFunction | null;
|
||||||
url?: string;
|
url?: string;
|
||||||
setFields?: React.Dispatch<React.SetStateAction<ApiFormFieldSet>>;
|
setFields?: React.Dispatch<React.SetStateAction<ApiFormFieldSet>>;
|
||||||
onKeyDown?: (value: any) => void;
|
onKeyDown?: (value: any) => void;
|
||||||
@@ -119,9 +122,10 @@ export function ApiFormField({
|
|||||||
case 'related field':
|
case 'related field':
|
||||||
return (
|
return (
|
||||||
<RelatedModelField
|
<RelatedModelField
|
||||||
controller={controller}
|
|
||||||
definition={fieldDefinition}
|
definition={fieldDefinition}
|
||||||
|
controller={controller}
|
||||||
fieldName={fieldName}
|
fieldName={fieldName}
|
||||||
|
navigate={navigate}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'email':
|
case 'email':
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
import { apiUrl } from '@lib/functions/Api';
|
import { apiUrl } from '@lib/functions/Api';
|
||||||
import type { ApiFormFieldType } from '@lib/types/Forms';
|
import type { ApiFormFieldType } from '@lib/types/Forms';
|
||||||
import { IconPlus } from '@tabler/icons-react';
|
import { IconPlus } from '@tabler/icons-react';
|
||||||
|
import type { NavigateFunction } from 'react-router-dom';
|
||||||
import { useApi } from '../../../contexts/ApiContext';
|
import { useApi } from '../../../contexts/ApiContext';
|
||||||
import { useCreateApiFormModal } from '../../../hooks/UseForm';
|
import { useCreateApiFormModal } from '../../../hooks/UseForm';
|
||||||
import {
|
import {
|
||||||
@@ -50,11 +51,13 @@ export function RelatedModelField({
|
|||||||
controller,
|
controller,
|
||||||
fieldName,
|
fieldName,
|
||||||
definition,
|
definition,
|
||||||
|
navigate,
|
||||||
limit = 10
|
limit = 10
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
controller: UseControllerReturn<FieldValues, any>;
|
controller: UseControllerReturn<FieldValues, any>;
|
||||||
definition: ApiFormFieldType;
|
definition: ApiFormFieldType;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
|
navigate?: NavigateFunction | null;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
}>) {
|
}>) {
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
|
|||||||
@@ -28,9 +28,13 @@ import type {
|
|||||||
} from '@lib/types/Rendering';
|
} from '@lib/types/Rendering';
|
||||||
|
|
||||||
export type { InstanceRenderInterface } from '@lib/types/Rendering';
|
export type { InstanceRenderInterface } from '@lib/types/Rendering';
|
||||||
import { getBaseUrl, navigateToLink, shortenString } from '@lib/index';
|
import {
|
||||||
|
getBaseUrl,
|
||||||
|
getDetailUrl,
|
||||||
|
navigateToLink,
|
||||||
|
shortenString
|
||||||
|
} from '@lib/index';
|
||||||
import { IconLink } from '@tabler/icons-react';
|
import { IconLink } from '@tabler/icons-react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { useApi } from '../../contexts/ApiContext';
|
import { useApi } from '../../contexts/ApiContext';
|
||||||
import { usePluginState } from '../../states/PluginState';
|
import { usePluginState } from '../../states/PluginState';
|
||||||
import { useUserSettingsState } from '../../states/SettingsStates';
|
import { useUserSettingsState } from '../../states/SettingsStates';
|
||||||
@@ -132,7 +136,6 @@ export function RenderInstance(props: RenderInstanceProps): ReactNode {
|
|||||||
props.custom_model ?? props.model ?? ''
|
props.custom_model ?? props.model ?? ''
|
||||||
);
|
);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const userSettings = useUserSettingsState();
|
const userSettings = useUserSettingsState();
|
||||||
|
|
||||||
// Extract model information from the defined model type
|
// Extract model information from the defined model type
|
||||||
@@ -170,12 +173,12 @@ export function RenderInstance(props: RenderInstanceProps): ReactNode {
|
|||||||
}, [modelInfo, props.instance]);
|
}, [modelInfo, props.instance]);
|
||||||
|
|
||||||
const detailUrl = useMemo(() => {
|
const detailUrl = useMemo(() => {
|
||||||
if (!modelInfo || !modelId || !modelInfo.url_detail) {
|
if (!props.model) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return modelInfo.url_detail.replace(':pk', modelId.toString());
|
return getDetailUrl(props.model, modelId, true);
|
||||||
}, [modelInfo, modelId]);
|
}, [props.model]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HoverCard
|
<HoverCard
|
||||||
@@ -207,7 +210,11 @@ export function RenderInstance(props: RenderInstanceProps): ReactNode {
|
|||||||
<Anchor
|
<Anchor
|
||||||
href={detailUrl}
|
href={detailUrl}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
onClick={(event) => navigateToLink(detailUrl, navigate, event)}
|
onClick={(event) => {
|
||||||
|
if (props.navigate) {
|
||||||
|
navigateToLink(detailUrl, props.navigate, event);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Group gap='xs' wrap='nowrap'>
|
<Group gap='xs' wrap='nowrap'>
|
||||||
<ActionIcon variant='transparent' size='xs'>
|
<ActionIcon variant='transparent' size='xs'>
|
||||||
|
|||||||
@@ -15,10 +15,13 @@ import {
|
|||||||
IconCalendar,
|
IconCalendar,
|
||||||
IconCalendarCheck,
|
IconCalendarCheck,
|
||||||
IconCalendarDot,
|
IconCalendarDot,
|
||||||
|
IconCalendarExclamation,
|
||||||
IconCalendarStats,
|
IconCalendarStats,
|
||||||
IconCalendarTime,
|
IconCalendarTime,
|
||||||
IconCalendarX,
|
IconCalendarX,
|
||||||
IconCancel,
|
IconCancel,
|
||||||
|
IconChartBar,
|
||||||
|
IconChartLine,
|
||||||
IconCheck,
|
IconCheck,
|
||||||
IconCircleCheck,
|
IconCircleCheck,
|
||||||
IconCircleDashedCheck,
|
IconCircleDashedCheck,
|
||||||
@@ -63,6 +66,7 @@ import {
|
|||||||
IconMapPin,
|
IconMapPin,
|
||||||
IconMapPinHeart,
|
IconMapPinHeart,
|
||||||
IconMinusVertical,
|
IconMinusVertical,
|
||||||
|
IconNews,
|
||||||
IconNotes,
|
IconNotes,
|
||||||
IconNumber123,
|
IconNumber123,
|
||||||
IconNumbers,
|
IconNumbers,
|
||||||
@@ -158,6 +162,7 @@ const icons: InvenTreeIconType = {
|
|||||||
transfer_orders: IconTransfer,
|
transfer_orders: IconTransfer,
|
||||||
sales_orders: IconTruckDelivery,
|
sales_orders: IconTruckDelivery,
|
||||||
scheduling: IconCalendarStats,
|
scheduling: IconCalendarStats,
|
||||||
|
overdue: IconCalendarExclamation,
|
||||||
scrap: IconCircleX,
|
scrap: IconCircleX,
|
||||||
shipment: IconCubeSend,
|
shipment: IconCubeSend,
|
||||||
test_templates: IconTestPipe,
|
test_templates: IconTestPipe,
|
||||||
@@ -267,7 +272,11 @@ const icons: InvenTreeIconType = {
|
|||||||
plugin: IconPlug,
|
plugin: IconPlug,
|
||||||
history: IconHistory,
|
history: IconHistory,
|
||||||
dashboard: IconLayoutDashboard,
|
dashboard: IconLayoutDashboard,
|
||||||
search: IconSearch
|
search: IconSearch,
|
||||||
|
|
||||||
|
chart_bar: IconChartBar,
|
||||||
|
chart_line: IconChartLine,
|
||||||
|
news: IconNews
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -258,23 +258,28 @@ export default function BarcodeScanHistoryTable() {
|
|||||||
icon={<IconExclamationCircle />}
|
icon={<IconExclamationCircle />}
|
||||||
title={t`Logging Disabled`}
|
title={t`Logging Disabled`}
|
||||||
>
|
>
|
||||||
<Text>{t`Barcode logging is not enabled`}</Text>
|
<Text>
|
||||||
|
{t`Barcode logging is not enabled.`}{' '}
|
||||||
|
{t`No barcode scan history will be recorded.`}
|
||||||
|
</Text>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
<InvenTreeTable
|
{globalSettings.isSet('BARCODE_STORE_RESULTS') && (
|
||||||
url={apiUrl(ApiEndpoints.barcode_history)}
|
<InvenTreeTable
|
||||||
tableState={table}
|
url={apiUrl(ApiEndpoints.barcode_history)}
|
||||||
columns={tableColumns}
|
tableState={table}
|
||||||
props={{
|
columns={tableColumns}
|
||||||
tableFilters: filters,
|
props={{
|
||||||
enableBulkDelete: canDelete,
|
tableFilters: filters,
|
||||||
rowActions: rowActions,
|
enableBulkDelete: canDelete,
|
||||||
onRowClick: (row) => {
|
rowActions: rowActions,
|
||||||
setSelectedResult(row);
|
onRowClick: (row) => {
|
||||||
open();
|
setSelectedResult(row);
|
||||||
}
|
open();
|
||||||
}}
|
}
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -168,9 +168,14 @@ test('Stock - Filters', async ({ browser }) => {
|
|||||||
// Filter by custom status code
|
// Filter by custom status code
|
||||||
await clearTableFilters(page);
|
await clearTableFilters(page);
|
||||||
await setTableChoiceFilter(page, 'Status', 'Incoming goods inspection');
|
await setTableChoiceFilter(page, 'Status', 'Incoming goods inspection');
|
||||||
await page.getByText('1 - 8 / 8').waitFor();
|
|
||||||
await page.getByRole('cell', { name: '1551AGY' }).first().waitFor();
|
await page.getByRole('cell', { name: '1551AGY' }).first().waitFor();
|
||||||
|
|
||||||
|
await page.getByPlaceholder('Search').clear();
|
||||||
|
await page.getByPlaceholder('Search').fill('blue');
|
||||||
await page.getByRole('cell', { name: 'widget.blue' }).first().waitFor();
|
await page.getByRole('cell', { name: 'widget.blue' }).first().waitFor();
|
||||||
|
|
||||||
|
await page.getByPlaceholder('Search').clear();
|
||||||
|
await page.getByPlaceholder('Search').fill('002.01');
|
||||||
await page.getByRole('cell', { name: '002.01-PCBA' }).first().waitFor();
|
await page.getByRole('cell', { name: '002.01-PCBA' }).first().waitFor();
|
||||||
|
|
||||||
await clearTableFilters(page);
|
await clearTableFilters(page);
|
||||||
@@ -324,10 +329,20 @@ test('Stock - Stock Actions', async ({ browser }) => {
|
|||||||
await page.getByRole('option', { name: status }).click();
|
await page.getByRole('option', { name: status }).click();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Duplicate the stock item first - prevent impacting other tests
|
||||||
|
await page
|
||||||
|
.getByRole('button', { name: 'action-menu-stock-item-actions' })
|
||||||
|
.click();
|
||||||
|
await page
|
||||||
|
.getByRole('menuitem', { name: 'action-menu-stock-item-actions-duplicate' })
|
||||||
|
.click();
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
// Check for required values
|
// Check for required values
|
||||||
await page.getByText('Status', { exact: true }).waitFor();
|
await page.getByText('Status', { exact: true }).waitFor();
|
||||||
await page.getByText('Custom Status', { exact: true }).waitFor();
|
await page.getByText('Custom Status', { exact: true }).waitFor();
|
||||||
await page.getByText('Attention needed').waitFor();
|
await page.getByText('Attention needed').first().waitFor();
|
||||||
await page
|
await page
|
||||||
.getByLabel('Stock Details')
|
.getByLabel('Stock Details')
|
||||||
.getByText('Incoming goods inspection')
|
.getByText('Incoming goods inspection')
|
||||||
@@ -705,6 +720,7 @@ test('Transfer Order - Allocate and Transfer', async ({ browser }) => {
|
|||||||
await page.getByRole('button', { name: 'Issue Order' }).click();
|
await page.getByRole('button', { name: 'Issue Order' }).click();
|
||||||
await page.getByRole('button', { name: 'Submit' }).click();
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
await page.getByText('Order issued').waitFor();
|
await page.getByText('Order issued').waitFor();
|
||||||
|
await page.getByText('Issued', { exact: true }).first().waitFor();
|
||||||
|
|
||||||
await loadTab(page, 'Line Items');
|
await loadTab(page, 'Line Items');
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,44 @@
|
|||||||
/** Unit tests for form validation, rendering, etc */
|
/** Unit tests for form validation, rendering, etc */
|
||||||
import { expect, test } from 'playwright/test';
|
import { expect, test } from 'playwright/test';
|
||||||
|
import { createApi } from './api';
|
||||||
import { stevenuser } from './defaults';
|
import { stevenuser } from './defaults';
|
||||||
import { navigate } from './helpers';
|
import { navigate } from './helpers';
|
||||||
import { doCachedLogin } from './login';
|
import { doCachedLogin } from './login';
|
||||||
|
|
||||||
|
// Test hover form action in related fields
|
||||||
|
test('Forms - Hover', async ({ browser }) => {
|
||||||
|
const page = await doCachedLogin(browser, {
|
||||||
|
user: stevenuser,
|
||||||
|
url: 'purchasing/index/purchaseorders'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Patch user settings to ensure we can see "extra model info" on hover
|
||||||
|
const api = await createApi({
|
||||||
|
username: stevenuser.username,
|
||||||
|
password: stevenuser.testcred
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await api.patch('settings/user/SHOW_EXTRA_MODEL_INFO/', {
|
||||||
|
data: {
|
||||||
|
value: 'true'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status()).toBe(200);
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByRole('button', { name: 'action-button-add-purchase-' })
|
||||||
|
.click();
|
||||||
|
await page.getByLabel('related-field-supplier').fill('mou');
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
await page.waitForTimeout(250);
|
||||||
|
await page.getByRole('option', { name: 'Mouser Electronics' }).hover();
|
||||||
|
|
||||||
|
// Check for hover info
|
||||||
|
await page.getByText('Company[ID: 2]').waitFor();
|
||||||
|
await page.getByRole('link', { name: 'View details' }).waitFor();
|
||||||
|
});
|
||||||
|
|
||||||
test('Forms - Stock Item Validation', async ({ browser }) => {
|
test('Forms - Stock Item Validation', async ({ browser }) => {
|
||||||
const page = await doCachedLogin(browser, {
|
const page = await doCachedLogin(browser, {
|
||||||
user: stevenuser,
|
user: stevenuser,
|
||||||
|
|||||||
@@ -253,3 +253,60 @@ test('Plugins - Locate Item', async ({ browser }) => {
|
|||||||
await page.getByRole('button', { name: 'Submit' }).click();
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
await page.getByText('Item location requested').waitFor();
|
await page.getByText('Item location requested').waitFor();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a full run through of validating a UI plugin:
|
||||||
|
*
|
||||||
|
* - Activate the plugin
|
||||||
|
* - Check that a custom panel is added
|
||||||
|
* - Check that expected translated text is added
|
||||||
|
* - Check that expected UI elements can be operated
|
||||||
|
*
|
||||||
|
* Note: This tests assumes that:
|
||||||
|
*
|
||||||
|
* - The inventree-plugin-creator tool has been installed
|
||||||
|
* - The default plugin has been created, build and installed
|
||||||
|
*/
|
||||||
|
test('Plugins - Creator', async ({ browser }) => {
|
||||||
|
const page = await doCachedLogin(browser, {
|
||||||
|
user: adminuser
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure that the SampleIntegration plugin is enabled
|
||||||
|
await setPluginState({
|
||||||
|
plugin: 'my-custom-plugin',
|
||||||
|
state: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Allow time for installation of plugin static files, etc
|
||||||
|
await page.waitForTimeout(2500);
|
||||||
|
|
||||||
|
await navigate(page, 'part/106/details/');
|
||||||
|
await loadTab(page, 'My Custom Plugin');
|
||||||
|
|
||||||
|
// Check for correctly translated code
|
||||||
|
await page.getByText('Translated text, provided by custom code!').waitFor();
|
||||||
|
|
||||||
|
// Check for incrementing counter value
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
await page.getByText(`Counter: ${i}`).waitFor();
|
||||||
|
await page.getByRole('button', { name: 'Increment Counter' }).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit part form
|
||||||
|
await page.getByRole('button', { name: 'Edit Part' }).click();
|
||||||
|
await page
|
||||||
|
.getByText('This is a custom form launched from within a plugin!')
|
||||||
|
.waitFor();
|
||||||
|
await page
|
||||||
|
.getByRole('textbox', { name: 'text-field-name' })
|
||||||
|
.fill('New part name');
|
||||||
|
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||||
|
|
||||||
|
// View a custom table
|
||||||
|
await page.getByRole('button', { name: 'Custom Table Example' }).click();
|
||||||
|
await page.getByRole('textbox', { name: 'table-search-input' }).fill('red');
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
await page.getByRole('cell', { name: 'Red Square Table' }).first().waitFor();
|
||||||
|
});
|
||||||
|
// Ensure that the sample full run plugin is enabled
|
||||||
|
|||||||
@@ -385,10 +385,20 @@ test('Settings - Admin - Barcode History', async ({ browser }) => {
|
|||||||
|
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
// Barcode history is displayed in table
|
const checkBarcode = async (barcode: string) => {
|
||||||
barcodes.forEach(async (barcode) => {
|
await page.getByRole('textbox', { name: 'table-search-input' }).clear();
|
||||||
|
await page
|
||||||
|
.getByRole('textbox', { name: 'table-search-input' })
|
||||||
|
.fill(barcode);
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
await page.getByText(barcode).first().waitFor();
|
await page.getByText(barcode).first().waitFor();
|
||||||
});
|
};
|
||||||
|
|
||||||
|
for (const barcode of barcodes) {
|
||||||
|
await checkBarcode(barcode);
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.waitForTimeout(2500);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Settings - Admin - Parameter', async ({ browser }) => {
|
test('Settings - Admin - Parameter', async ({ browser }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user