mirror of
https://github.com/inventree/InvenTree.git
synced 2026-07-21 22:23:03 +00:00
Merge commit 'fcbe4302b9fda7682b1a85e7d315b2d1545a16bf' into line-discount
This commit is contained in:
@@ -1,27 +1,35 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Alert, Stack, Text } from '@mantine/core';
|
||||
import { ErrorBoundary, type FallbackRender } from '@sentry/react';
|
||||
import { IconExclamationCircle } from '@tabler/icons-react';
|
||||
import { type ReactNode, useCallback } from 'react';
|
||||
import { IconExclamationCircle, IconInfoCircle } from '@tabler/icons-react';
|
||||
import { type ReactNode, useCallback, useState } from 'react';
|
||||
|
||||
export function DefaultFallback({
|
||||
title
|
||||
}: Readonly<{ title: string }>): ReactNode {
|
||||
title,
|
||||
error
|
||||
}: Readonly<{ title: string; error: string | null }>): ReactNode {
|
||||
return (
|
||||
<Alert
|
||||
color='red'
|
||||
icon={<IconExclamationCircle />}
|
||||
title={`INVE-E17: ${t`Error rendering component`}: ${title}`}
|
||||
>
|
||||
<Stack gap='xs'>
|
||||
<Text size='sm'>
|
||||
{t`An error occurred while rendering this component. Refer to the console for more information.`}
|
||||
</Text>
|
||||
<Text size='sm'>
|
||||
{t`Try reloading the page, or contact your administrator if the problem persists.`}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Alert>
|
||||
<>
|
||||
<Alert
|
||||
color='red'
|
||||
icon={<IconExclamationCircle />}
|
||||
title={`INVE-E17: ${t`Error rendering component`}: ${title}`}
|
||||
>
|
||||
<Stack gap='xs'>
|
||||
<Text size='sm'>
|
||||
{t`An error occurred while rendering this component.`}
|
||||
</Text>
|
||||
<Text size='sm'>
|
||||
{t`Try reloading the page, or contact your administrator if the problem persists.`}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Alert>
|
||||
{error && (
|
||||
<Alert color='red' icon={<IconInfoCircle />} title={t`Error Details`}>
|
||||
<Text size='sm'>{error}</Text>
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,17 +42,22 @@ export function Boundary({
|
||||
label: string;
|
||||
fallback?: React.ReactElement<any> | FallbackRender;
|
||||
}>): ReactNode {
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const onError = useCallback(
|
||||
(error: unknown, componentStack: string | undefined, eventId: string) => {
|
||||
console.error(`ERR: Error rendering component: ${label}`);
|
||||
console.error(error);
|
||||
setErrorMessage(error instanceof Error ? error.message : String(error));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={fallback ?? <DefaultFallback title={label} />}
|
||||
fallback={
|
||||
fallback ?? <DefaultFallback title={label} error={errorMessage} />
|
||||
}
|
||||
onError={onError}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Group, Text } from '@mantine/core';
|
||||
import type { ReactNode } from 'react';
|
||||
import { formatDecimal } from '../../defaults/formatters';
|
||||
import { TableHoverCard } from './TableHoverCard';
|
||||
|
||||
export function renderPartStockCell(record: any): ReactNode {
|
||||
if (record.virtual) {
|
||||
return (
|
||||
<Text size='sm' c='dimmed' fs='italic'>
|
||||
{t`Virtual part`}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const extra: ReactNode[] = [];
|
||||
|
||||
const stock = record?.total_in_stock ?? 0;
|
||||
const allocated =
|
||||
(record?.allocated_to_build_orders ?? 0) +
|
||||
(record?.allocated_to_sales_orders ?? 0);
|
||||
const available = Math.max(0, stock - allocated);
|
||||
const min_stock = record?.minimum_stock ?? 0;
|
||||
const max_stock = record?.maximum_stock ?? 0;
|
||||
|
||||
let text = String(formatDecimal(stock));
|
||||
|
||||
let color: string | undefined = undefined;
|
||||
|
||||
if (min_stock > stock) {
|
||||
extra.push(
|
||||
<Text key='min-stock' c='orange'>
|
||||
{`${t`Minimum stock`}: ${formatDecimal(min_stock)}`}
|
||||
</Text>
|
||||
);
|
||||
|
||||
color = 'orange';
|
||||
}
|
||||
|
||||
if (max_stock > 0 && stock > max_stock) {
|
||||
extra.push(
|
||||
<Text key='max-stock' c='teal'>
|
||||
{`${t`Maximum stock`}: ${formatDecimal(max_stock)}`}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (record.ordering > 0) {
|
||||
extra.push(
|
||||
<Text key='on-order'>{`${t`On Order`}: ${formatDecimal(record.ordering)}`}</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (record.building) {
|
||||
extra.push(
|
||||
<Text key='building'>{`${t`Building`}: ${formatDecimal(record.building)}`}</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (record.allocated_to_build_orders > 0) {
|
||||
extra.push(
|
||||
<Text key='bo-allocations'>
|
||||
{`${t`Build Order Allocations`}: ${formatDecimal(record.allocated_to_build_orders)}`}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (record.allocated_to_sales_orders > 0) {
|
||||
extra.push(
|
||||
<Text key='so-allocations'>
|
||||
{`${t`Sales Order Allocations`}: ${formatDecimal(record.allocated_to_sales_orders)}`}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (available != stock) {
|
||||
extra.push(
|
||||
<Text key='available'>
|
||||
{t`Available`}: {formatDecimal(available)}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (record.external_stock > 0) {
|
||||
extra.push(
|
||||
<Text key='external'>
|
||||
{t`External stock`}: {formatDecimal(record.external_stock)}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if ((record.variant_stock ?? 0) > 0) {
|
||||
extra.push(
|
||||
<Text key='variant-stock' size='sm'>
|
||||
{t`Includes variant stock`}: {formatDecimal(record.variant_stock)}
|
||||
</Text>
|
||||
);
|
||||
extra.push(
|
||||
<Text key='direct-stock' size='sm'>
|
||||
{t`Direct stock`}: {formatDecimal(record.in_stock ?? 0)}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (stock <= 0) {
|
||||
color = 'red';
|
||||
text = t`No stock`;
|
||||
} else if (available <= 0) {
|
||||
color = 'orange';
|
||||
} else if (available < min_stock) {
|
||||
color = 'yellow';
|
||||
}
|
||||
|
||||
return (
|
||||
<TableHoverCard
|
||||
value={
|
||||
<Group gap='xs' justify='left' wrap='nowrap'>
|
||||
<Text c={color} size='sm'>
|
||||
{text}
|
||||
</Text>
|
||||
{record.units && (
|
||||
<Text size='xs' c={color}>
|
||||
[{record.units}]
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
}
|
||||
title={t`Stock Information`}
|
||||
extra={extra}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+2061
-1965
File diff suppressed because it is too large
Load Diff
+1990
-1894
File diff suppressed because it is too large
Load Diff
+2086
-1990
File diff suppressed because it is too large
Load Diff
+2060
-1964
File diff suppressed because it is too large
Load Diff
+2057
-1961
File diff suppressed because it is too large
Load Diff
+2059
-1963
File diff suppressed because it is too large
Load Diff
+2060
-1964
File diff suppressed because it is too large
Load Diff
+2057
-1961
File diff suppressed because it is too large
Load Diff
+2047
-1951
File diff suppressed because it is too large
Load Diff
+2017
-1921
File diff suppressed because it is too large
Load Diff
+1990
-1894
File diff suppressed because it is too large
Load Diff
+1990
-1894
File diff suppressed because it is too large
Load Diff
+2060
-1964
File diff suppressed because it is too large
Load Diff
+1990
-1894
File diff suppressed because it is too large
Load Diff
+1990
-1894
File diff suppressed because it is too large
Load Diff
+2058
-1962
File diff suppressed because it is too large
Load Diff
+2002
-1906
File diff suppressed because it is too large
Load Diff
+2058
-1962
File diff suppressed because it is too large
Load Diff
+2061
-1965
File diff suppressed because it is too large
Load Diff
+2210
-2114
File diff suppressed because it is too large
Load Diff
+1991
-1895
File diff suppressed because it is too large
Load Diff
+1990
-1894
File diff suppressed because it is too large
Load Diff
+2058
-1962
File diff suppressed because it is too large
Load Diff
+2028
-1932
File diff suppressed because it is too large
Load Diff
+1992
-1896
File diff suppressed because it is too large
Load Diff
+2050
-1954
File diff suppressed because it is too large
Load Diff
+2063
-1967
File diff suppressed because it is too large
Load Diff
+1996
-1900
File diff suppressed because it is too large
Load Diff
+2228
-2132
File diff suppressed because it is too large
Load Diff
+1990
-1894
File diff suppressed because it is too large
Load Diff
+1990
-1894
File diff suppressed because it is too large
Load Diff
+2052
-1956
File diff suppressed because it is too large
Load Diff
+2034
-1938
File diff suppressed because it is too large
Load Diff
+1990
-1894
File diff suppressed because it is too large
Load Diff
+2048
-1952
File diff suppressed because it is too large
Load Diff
+2018
-1922
File diff suppressed because it is too large
Load Diff
+2037
-1941
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -45,9 +45,12 @@ export function SecurityContent() {
|
||||
|
||||
const user = useUserState();
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const onError = useCallback(
|
||||
(error: unknown, componentStack: string | undefined, eventId: string) => {
|
||||
console.error(`ERR: Error rendering component: ${error}`);
|
||||
setErrorMessage(error instanceof Error ? error.message : String(error));
|
||||
},
|
||||
[]
|
||||
);
|
||||
@@ -95,7 +98,9 @@ export function SecurityContent() {
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<ErrorBoundary
|
||||
fallback={<DefaultFallback title={'API Table'} />}
|
||||
fallback={
|
||||
<DefaultFallback title={'API Table'} error={errorMessage} />
|
||||
}
|
||||
onError={onError}
|
||||
>
|
||||
<ApiTokenTable only_myself />
|
||||
|
||||
@@ -390,7 +390,8 @@ export default function SystemSettings() {
|
||||
keys={[
|
||||
'TRANSFERORDER_ENABLED',
|
||||
'TRANSFERORDER_REFERENCE_PATTERN',
|
||||
'TRANSFERORDER_REQUIRE_RESPONSIBLE'
|
||||
'TRANSFERORDER_REQUIRE_RESPONSIBLE',
|
||||
'TRANSFERORDER_EDIT_COMPLETED_ORDERS'
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -69,13 +69,11 @@ export default function TransferOrderDetail() {
|
||||
const orderOpen: boolean =
|
||||
order.status != toStatus.COMPLETE && order.status != toStatus.CANCELLED;
|
||||
|
||||
return orderOpen;
|
||||
// TODO: does this setting make any sense for Transfer Orders???
|
||||
// if (orderOpen) {
|
||||
// return true;
|
||||
// } else {
|
||||
// return globalSettings.isSet('TRANSFERORDER_EDIT_COMPLETED_ORDERS');
|
||||
// }
|
||||
if (orderOpen) {
|
||||
return true;
|
||||
} else {
|
||||
return globalSettings.isSet('TRANSFERORDER_EDIT_COMPLETED_ORDERS');
|
||||
}
|
||||
}, [globalSettings, order.status, toStatus]);
|
||||
|
||||
// for now, only permit editing allocations when line items can be edited
|
||||
|
||||
@@ -184,6 +184,9 @@ export default function ExtraLineItemTable({
|
||||
params: {
|
||||
order: orderId
|
||||
},
|
||||
enableSelection: true,
|
||||
enableBulkDelete: editable && user.hasDeleteRole(role),
|
||||
afterBulkDelete: orderDetailRefresh,
|
||||
defaultSortColumn: 'line',
|
||||
rowActions: rowActions,
|
||||
tableActions: tableActions
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
DescriptionColumn,
|
||||
PartColumn
|
||||
} from '../../components/tables/ColumnRenderers';
|
||||
import { renderPartStockCell } from '../../components/tables/PartStockCell';
|
||||
import ParametricDataTable from '../general/ParametricDataTable';
|
||||
import { PartTableFilters } from './PartTableFilters';
|
||||
|
||||
@@ -33,7 +34,8 @@ export default function ParametricPartTable({
|
||||
},
|
||||
{
|
||||
accessor: 'total_in_stock',
|
||||
sortable: true
|
||||
sortable: true,
|
||||
render: renderPartStockCell
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
|
||||
@@ -12,14 +12,13 @@ import type { ApiFormFieldSet } from '@lib/types/Forms';
|
||||
import type { TableColumn } from '@lib/types/Tables';
|
||||
import type { InvenTreeTableProps } from '@lib/types/Tables';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Group, Text } from '@mantine/core';
|
||||
import {
|
||||
IconFileUpload,
|
||||
IconPackageImport,
|
||||
IconPlus,
|
||||
IconShoppingCart
|
||||
} from '@tabler/icons-react';
|
||||
import { type ReactNode, useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { ActionDropdown } from '../../components/items/ActionDropdown';
|
||||
import {
|
||||
BooleanColumn,
|
||||
@@ -31,10 +30,10 @@ import {
|
||||
PartColumn
|
||||
} from '../../components/tables/ColumnRenderers';
|
||||
import { InvenTreeTable } from '../../components/tables/InvenTreeTable';
|
||||
import { TableHoverCard } from '../../components/tables/TableHoverCard';
|
||||
import { renderPartStockCell } from '../../components/tables/PartStockCell';
|
||||
import ImportPartWizard from '../../components/wizards/ImportPartWizard';
|
||||
import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
|
||||
import { formatDecimal, formatPriceRange } from '../../defaults/formatters';
|
||||
import { formatPriceRange } from '../../defaults/formatters';
|
||||
import { DuplicateField } from '../../forms/CommonFields';
|
||||
import { dataImporterSessionFields } from '../../forms/ImporterForms';
|
||||
import { usePartFields } from '../../forms/PartForms';
|
||||
@@ -84,119 +83,7 @@ function partTableColumns(): TableColumn[] {
|
||||
accessor: 'total_in_stock',
|
||||
sortable: true,
|
||||
filter: ['has_stock', 'low_stock', 'high_stock'],
|
||||
render: (record) => {
|
||||
if (record.virtual) {
|
||||
return (
|
||||
<Text size='sm' c='dimmed' fs='italic'>
|
||||
{t`Virtual part`}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const extra: ReactNode[] = [];
|
||||
|
||||
const stock = record?.total_in_stock ?? 0;
|
||||
const allocated =
|
||||
(record?.allocated_to_build_orders ?? 0) +
|
||||
(record?.allocated_to_sales_orders ?? 0);
|
||||
const available = Math.max(0, stock - allocated);
|
||||
const min_stock = record?.minimum_stock ?? 0;
|
||||
const max_stock = record?.maximum_stock ?? 0;
|
||||
|
||||
let text = String(formatDecimal(stock));
|
||||
|
||||
let color: string | undefined = undefined;
|
||||
|
||||
if (min_stock > stock) {
|
||||
extra.push(
|
||||
<Text key='min-stock' c='orange'>
|
||||
{`${t`Minimum stock`}: ${formatDecimal(min_stock)}`}
|
||||
</Text>
|
||||
);
|
||||
|
||||
color = 'orange';
|
||||
}
|
||||
|
||||
if (max_stock > 0 && stock > max_stock) {
|
||||
extra.push(
|
||||
<Text key='max-stock' c='teal'>
|
||||
{`${t`Maximum stock`}: ${formatDecimal(max_stock)}`}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (record.ordering > 0) {
|
||||
extra.push(
|
||||
<Text key='on-order'>{`${t`On Order`}: ${formatDecimal(record.ordering)}`}</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (record.building) {
|
||||
extra.push(
|
||||
<Text key='building'>{`${t`Building`}: ${formatDecimal(record.building)}`}</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (record.allocated_to_build_orders > 0) {
|
||||
extra.push(
|
||||
<Text key='bo-allocations'>
|
||||
{`${t`Build Order Allocations`}: ${formatDecimal(record.allocated_to_build_orders)}`}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (record.allocated_to_sales_orders > 0) {
|
||||
extra.push(
|
||||
<Text key='so-allocations'>
|
||||
{`${t`Sales Order Allocations`}: ${formatDecimal(record.allocated_to_sales_orders)}`}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (available != stock) {
|
||||
extra.push(
|
||||
<Text key='available'>
|
||||
{t`Available`}: {formatDecimal(available)}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (record.external_stock > 0) {
|
||||
extra.push(
|
||||
<Text key='external'>
|
||||
{t`External stock`}: {formatDecimal(record.external_stock)}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (stock <= 0) {
|
||||
color = 'red';
|
||||
text = t`No stock`;
|
||||
} else if (available <= 0) {
|
||||
color = 'orange';
|
||||
} else if (available < min_stock) {
|
||||
color = 'yellow';
|
||||
}
|
||||
|
||||
return (
|
||||
<TableHoverCard
|
||||
value={
|
||||
<Group gap='xs' justify='left' wrap='nowrap'>
|
||||
<Text c={color} size='sm'>
|
||||
{text}
|
||||
</Text>
|
||||
{record.units && (
|
||||
<Text size='xs' c={color}>
|
||||
[{record.units}]
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
}
|
||||
title={t`Stock Information`}
|
||||
extra={extra}
|
||||
/>
|
||||
);
|
||||
}
|
||||
render: renderPartStockCell
|
||||
},
|
||||
{
|
||||
accessor: 'price_range',
|
||||
|
||||
@@ -440,6 +440,9 @@ export function PurchaseOrderLineItemTable({
|
||||
props={{
|
||||
enableSelection: true,
|
||||
enableDownload: true,
|
||||
enableBulkDelete:
|
||||
editable && user.hasDeleteRole(UserRoles.purchase_order),
|
||||
afterBulkDelete: orderDetailRefresh,
|
||||
defaultSortColumn: 'line',
|
||||
params: {
|
||||
...params,
|
||||
|
||||
@@ -280,6 +280,9 @@ export default function ReturnOrderLineItemTable({
|
||||
defaultSortColumn: 'line',
|
||||
enableSelection:
|
||||
inProgress && user.hasChangeRole(UserRoles.return_order),
|
||||
enableBulkDelete:
|
||||
editable && user.hasDeleteRole(UserRoles.return_order),
|
||||
afterBulkDelete: orderDetailRefresh,
|
||||
tableActions: tableActions,
|
||||
tableFilters: tableFilters,
|
||||
rowActions: rowActions,
|
||||
|
||||
@@ -615,6 +615,9 @@ export default function SalesOrderLineItemTable({
|
||||
props={{
|
||||
enableSelection: true,
|
||||
enableDownload: true,
|
||||
enableBulkDelete:
|
||||
editable && user.hasDeleteRole(UserRoles.sales_order),
|
||||
afterBulkDelete: orderDetailRefresh,
|
||||
params: {
|
||||
order: orderId,
|
||||
part_detail: true
|
||||
|
||||
@@ -517,6 +517,9 @@ export default function TransferOrderLineItemTable({
|
||||
props={{
|
||||
enableSelection: true,
|
||||
enableDownload: true,
|
||||
enableBulkDelete:
|
||||
editable && user.hasDeleteRole(UserRoles.transfer_order),
|
||||
afterBulkDelete: orderDetailRefresh,
|
||||
params: {
|
||||
order: orderId,
|
||||
part_detail: true
|
||||
|
||||
@@ -283,7 +283,7 @@ test('Build Order - Build Outputs', async ({ browser }) => {
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Check the "printing" actions for the selected outputs
|
||||
await page.getByRole('checkbox', { name: 'Select all records' }).check();
|
||||
await page.getByRole('checkbox', { name: 'Select all records' }).click();
|
||||
await page
|
||||
.getByRole('tabpanel', { name: 'Incomplete Outputs' })
|
||||
.getByLabel('action-menu-printing-actions')
|
||||
|
||||
@@ -507,7 +507,12 @@ test('Purchase Orders - Receive Items', async ({ browser }) => {
|
||||
await loadTab(page, 'Line Items');
|
||||
|
||||
await page.getByRole('cell', { name: '002.02-PCB' }).waitFor();
|
||||
await page.getByLabel('Select all records').click();
|
||||
|
||||
// Select all line items to receive
|
||||
await page
|
||||
.getByRole('region', { name: 'Line Items', exact: true })
|
||||
.getByLabel('Select all records')
|
||||
.click();
|
||||
await page.waitForTimeout(100);
|
||||
await page.getByLabel('action-button-receive-items').click();
|
||||
|
||||
@@ -619,7 +624,10 @@ test('Purchase Orders - Receive Virtual Items', async ({ browser }) => {
|
||||
await loadTab(page, 'Line Items');
|
||||
await page.getByRole('cell', { name: 'Thumbnail CRM license' }).waitFor();
|
||||
|
||||
await page.getByRole('checkbox', { name: 'Select all records' }).click();
|
||||
await page
|
||||
.getByRole('region', { name: 'Line Items', exact: true })
|
||||
.getByLabel('Select all records')
|
||||
.click();
|
||||
await page
|
||||
.getByRole('button', { name: 'action-button-receive-items' })
|
||||
.click();
|
||||
|
||||
@@ -14,7 +14,10 @@ test('Return Orders - Receive Items', async ({ browser }) => {
|
||||
|
||||
await page.getByRole('cell', { name: 'WID-REV-A' }).first().waitFor();
|
||||
|
||||
await page.getByRole('checkbox', { name: 'Select all records' }).click();
|
||||
await page
|
||||
.getByRole('region', { name: 'Line Items', exact: true })
|
||||
.getByLabel('Select all records')
|
||||
.click();
|
||||
await page.getByRole('button', { name: 'action-button-receive-' }).click();
|
||||
await page.getByRole('banner').getByText('Receive Items').waitFor();
|
||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||
|
||||
Reference in New Issue
Block a user