mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-09 08:48:48 +00:00
[UI] Search Improvements (#9137)
* Harden playwright tests * Refactor search drawer - Allow result groups to collapse * Add tooltip * Fix menu position * Navigate through to complete list of results * Refactor table headers * Add index pages for SupplierPart and ManufacturerPart models * backend: allow split search by company type * Fix panel naming bug * Fix model URLs * Split company results by company type - Allows better routing to results list * Remove debug msg * Fix 'button within button' issue * Additional playwright tests
This commit is contained in:
parent
09cdf72bda
commit
44cca7ddf2
@ -522,6 +522,9 @@ class APISearchView(GenericAPIView):
|
|||||||
return {
|
return {
|
||||||
'build': build.api.BuildList,
|
'build': build.api.BuildList,
|
||||||
'company': company.api.CompanyList,
|
'company': company.api.CompanyList,
|
||||||
|
'supplier': company.api.CompanyList,
|
||||||
|
'manufacturer': company.api.CompanyList,
|
||||||
|
'customer': company.api.CompanyList,
|
||||||
'manufacturerpart': company.api.ManufacturerPartList,
|
'manufacturerpart': company.api.ManufacturerPartList,
|
||||||
'supplierpart': company.api.SupplierPartList,
|
'supplierpart': company.api.SupplierPartList,
|
||||||
'part': part.api.PartList,
|
'part': part.api.PartList,
|
||||||
@ -534,6 +537,14 @@ class APISearchView(GenericAPIView):
|
|||||||
'stocklocation': stock.api.StockLocationList,
|
'stocklocation': stock.api.StockLocationList,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_result_filters(self):
|
||||||
|
"""Provide extra filtering options for particular search groups."""
|
||||||
|
return {
|
||||||
|
'supplier': {'is_supplier': True},
|
||||||
|
'manufacturer': {'is_manufacturer': True},
|
||||||
|
'customer': {'is_customer': True},
|
||||||
|
}
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
"""Perform search query against available models."""
|
"""Perform search query against available models."""
|
||||||
data = request.data
|
data = request.data
|
||||||
@ -552,6 +563,8 @@ class APISearchView(GenericAPIView):
|
|||||||
if 'search' not in data:
|
if 'search' not in data:
|
||||||
raise ValidationError({'search': 'Search term must be provided'})
|
raise ValidationError({'search': 'Search term must be provided'})
|
||||||
|
|
||||||
|
search_filters = self.get_result_filters()
|
||||||
|
|
||||||
for key, cls in self.get_result_types().items():
|
for key, cls in self.get_result_types().items():
|
||||||
# Only return results which are specifically requested
|
# Only return results which are specifically requested
|
||||||
if key in data:
|
if key in data:
|
||||||
@ -560,6 +573,11 @@ class APISearchView(GenericAPIView):
|
|||||||
for k, v in pass_through_params.items():
|
for k, v in pass_through_params.items():
|
||||||
params[k] = request.data.get(k, v)
|
params[k] = request.data.get(k, v)
|
||||||
|
|
||||||
|
# Add in any extra filters for this particular search type
|
||||||
|
if key in search_filters:
|
||||||
|
for k, v in search_filters[key].items():
|
||||||
|
params[k] = v
|
||||||
|
|
||||||
# Enforce json encoding
|
# Enforce json encoding
|
||||||
params['format'] = 'json'
|
params['format'] = 'json'
|
||||||
|
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
import { Trans, t } from '@lingui/macro';
|
import { Trans, t } from '@lingui/macro';
|
||||||
import {
|
import {
|
||||||
|
Accordion,
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Alert,
|
Alert,
|
||||||
Anchor,
|
Anchor,
|
||||||
Center,
|
Center,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Divider,
|
|
||||||
Drawer,
|
Drawer,
|
||||||
Group,
|
Group,
|
||||||
Loader,
|
Loader,
|
||||||
Menu,
|
Menu,
|
||||||
Paper,
|
|
||||||
Space,
|
Space,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
@ -21,19 +20,23 @@ import { useDebouncedValue } from '@mantine/hooks';
|
|||||||
import {
|
import {
|
||||||
IconAlertCircle,
|
IconAlertCircle,
|
||||||
IconBackspace,
|
IconBackspace,
|
||||||
|
IconExclamationCircle,
|
||||||
IconRefresh,
|
IconRefresh,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
|
IconTableExport,
|
||||||
IconX
|
IconX
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { type NavigateFunction, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { showNotification } from '@mantine/notifications';
|
||||||
import { api } from '../../App';
|
import { api } from '../../App';
|
||||||
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';
|
||||||
|
import { cancelEvent } from '../../functions/events';
|
||||||
import { navigateToLink } from '../../functions/navigation';
|
import { navigateToLink } from '../../functions/navigation';
|
||||||
import { apiUrl } from '../../states/ApiState';
|
import { apiUrl } from '../../states/ApiState';
|
||||||
import { useUserSettingsState } from '../../states/SettingsState';
|
import { useUserSettingsState } from '../../states/SettingsState';
|
||||||
@ -45,6 +48,9 @@ import { ModelInformationDict, getModelInfo } from '../render/ModelType';
|
|||||||
// Define type for handling individual search queries
|
// Define type for handling individual search queries
|
||||||
type SearchQuery = {
|
type SearchQuery = {
|
||||||
model: ModelType;
|
model: ModelType;
|
||||||
|
searchKey?: string;
|
||||||
|
title?: string;
|
||||||
|
overviewUrl?: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
parameters: any;
|
parameters: any;
|
||||||
results?: any;
|
results?: any;
|
||||||
@ -54,50 +60,102 @@ type SearchQuery = {
|
|||||||
* Render the results for a single search query
|
* Render the results for a single search query
|
||||||
*/
|
*/
|
||||||
function QueryResultGroup({
|
function QueryResultGroup({
|
||||||
|
searchText,
|
||||||
query,
|
query,
|
||||||
|
navigate,
|
||||||
|
onClose,
|
||||||
onRemove,
|
onRemove,
|
||||||
onResultClick
|
onResultClick
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
|
searchText: string;
|
||||||
query: SearchQuery;
|
query: SearchQuery;
|
||||||
|
navigate: NavigateFunction;
|
||||||
|
onClose: () => void;
|
||||||
onRemove: (query: ModelType) => void;
|
onRemove: (query: ModelType) => void;
|
||||||
onResultClick: (query: ModelType, pk: number, event: any) => void;
|
onResultClick: (query: ModelType, pk: number, event: any) => void;
|
||||||
}>) {
|
}>) {
|
||||||
|
const modelInfo = useMemo(() => getModelInfo(query.model), [query.model]);
|
||||||
|
|
||||||
|
const overviewUrl: string | undefined = useMemo(() => {
|
||||||
|
// Query has a custom overview URL
|
||||||
|
if (query.overviewUrl) {
|
||||||
|
return query.overviewUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return modelInfo.url_overview;
|
||||||
|
}, [query, modelInfo]);
|
||||||
|
|
||||||
|
// Callback function to view all results for a given query
|
||||||
|
const viewResults = useCallback(
|
||||||
|
(event: any) => {
|
||||||
|
cancelEvent(event);
|
||||||
|
|
||||||
|
if (overviewUrl) {
|
||||||
|
const url = `${overviewUrl}?search=${searchText}`;
|
||||||
|
|
||||||
|
// Close drawer if opening in the same tab
|
||||||
|
if (!(event?.ctrlKey || event?.shiftKey)) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateToLink(url, navigate, event);
|
||||||
|
} else {
|
||||||
|
showNotification({
|
||||||
|
title: t`No Overview Available`,
|
||||||
|
message: t`No overview available for this model type`,
|
||||||
|
color: 'red',
|
||||||
|
icon: <IconExclamationCircle />
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[overviewUrl, searchText]
|
||||||
|
);
|
||||||
|
|
||||||
if (query.results.count == 0) {
|
if (query.results.count == 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const model = getModelInfo(query.model);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Accordion.Item key={query.model} value={query.model}>
|
||||||
withBorder
|
<Accordion.Control component='div'>
|
||||||
shadow='sm'
|
<Group justify='space-between'>
|
||||||
p='md'
|
|
||||||
key={`paper-${query.model}`}
|
|
||||||
aria-label={`search-group-${query.model}`}
|
|
||||||
>
|
|
||||||
<Stack key={`stack-${query.model}`}>
|
|
||||||
<Group justify='space-between' wrap='nowrap'>
|
|
||||||
<Group justify='left' gap={5} wrap='nowrap'>
|
<Group justify='left' gap={5} wrap='nowrap'>
|
||||||
<Text size='lg'>{model.label_multiple}</Text>
|
<Tooltip label={t`View all results`} position='top-start'>
|
||||||
|
<ActionIcon
|
||||||
|
size='sm'
|
||||||
|
variant='transparent'
|
||||||
|
radius='xs'
|
||||||
|
aria-label={`view-all-results-${query.searchKey ?? query.model}`}
|
||||||
|
disabled={!overviewUrl}
|
||||||
|
onClick={viewResults}
|
||||||
|
>
|
||||||
|
<IconTableExport />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<Text size='lg'>{query.title ?? modelInfo.label_multiple}</Text>
|
||||||
<Text size='sm' style={{ fontStyle: 'italic' }}>
|
<Text size='sm' style={{ fontStyle: 'italic' }}>
|
||||||
{' '}
|
{' '}
|
||||||
- {query.results.count} <Trans>results</Trans>
|
- {query.results.count} <Trans>results</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Space />
|
<Group justify='right' wrap='nowrap'>
|
||||||
<ActionIcon
|
<Tooltip label={t`Remove search group`} position='top-end'>
|
||||||
size='sm'
|
<ActionIcon
|
||||||
color='red'
|
size='sm'
|
||||||
variant='transparent'
|
color='red'
|
||||||
radius='xs'
|
variant='transparent'
|
||||||
aria-label={`remove-search-group-${query.model}`}
|
radius='xs'
|
||||||
onClick={() => onRemove(query.model)}
|
aria-label={`remove-search-group-${query.model}`}
|
||||||
>
|
onClick={() => onRemove(query.model)}
|
||||||
<IconX />
|
>
|
||||||
</ActionIcon>
|
<IconX />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<Space />
|
||||||
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
<Divider />
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
<Stack aria-label={`search-group-results-${query.model}`}>
|
<Stack aria-label={`search-group-results-${query.model}`}>
|
||||||
{query.results.results.map((result: any) => (
|
{query.results.results.map((result: any) => (
|
||||||
<Anchor
|
<Anchor
|
||||||
@ -111,9 +169,8 @@ function QueryResultGroup({
|
|||||||
</Anchor>
|
</Anchor>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
<Space />
|
</Accordion.Panel>
|
||||||
</Stack>
|
</Accordion.Item>
|
||||||
</Paper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,10 +270,32 @@ export function SearchDrawer({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
model: ModelType.company,
|
model: ModelType.company,
|
||||||
|
overviewUrl: '/purchasing/index/suppliers',
|
||||||
|
searchKey: 'supplier',
|
||||||
|
title: t`Suppliers`,
|
||||||
parameters: {},
|
parameters: {},
|
||||||
enabled:
|
enabled:
|
||||||
(user.hasViewRole(UserRoles.sales_order) ||
|
user.hasViewRole(UserRoles.purchase_order) &&
|
||||||
user.hasViewRole(UserRoles.purchase_order)) &&
|
userSettings.isSet('SEARCH_PREVIEW_SHOW_COMPANIES')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: ModelType.company,
|
||||||
|
overviewUrl: '/purchasing/index/manufacturers',
|
||||||
|
searchKey: 'manufacturer',
|
||||||
|
title: t`Manufacturers`,
|
||||||
|
parameters: {},
|
||||||
|
enabled:
|
||||||
|
user.hasViewRole(UserRoles.purchase_order) &&
|
||||||
|
userSettings.isSet('SEARCH_PREVIEW_SHOW_COMPANIES')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: ModelType.company,
|
||||||
|
overviewUrl: '/sales/index/customers',
|
||||||
|
searchKey: 'customer',
|
||||||
|
title: t`Customers`,
|
||||||
|
parameters: {},
|
||||||
|
enabled:
|
||||||
|
user.hasViewRole(UserRoles.sales_order) &&
|
||||||
userSettings.isSet('SEARCH_PREVIEW_SHOW_COMPANIES')
|
userSettings.isSet('SEARCH_PREVIEW_SHOW_COMPANIES')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -272,7 +351,9 @@ export function SearchDrawer({
|
|||||||
}, [user, userSettings]);
|
}, [user, userSettings]);
|
||||||
|
|
||||||
// Construct a list of search queries based on user permissions
|
// Construct a list of search queries based on user permissions
|
||||||
const searchQueries: SearchQuery[] = searchQueryList.filter((q) => q.enabled);
|
const searchQueries: SearchQuery[] = useMemo(() => {
|
||||||
|
return searchQueryList.filter((q) => q.enabled);
|
||||||
|
}, [searchQueryList]);
|
||||||
|
|
||||||
// Re-fetch data whenever the search term is updated
|
// Re-fetch data whenever the search term is updated
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -296,7 +377,8 @@ export function SearchDrawer({
|
|||||||
|
|
||||||
// Add in custom query parameters
|
// Add in custom query parameters
|
||||||
searchQueries.forEach((query) => {
|
searchQueries.forEach((query) => {
|
||||||
params[query.model] = query.parameters;
|
const key = query.searchKey || query.model;
|
||||||
|
params[key] = query.parameters;
|
||||||
});
|
});
|
||||||
|
|
||||||
return api
|
return api
|
||||||
@ -321,11 +403,11 @@ export function SearchDrawer({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchQuery.data) {
|
if (searchQuery.data) {
|
||||||
let queries = searchQueries.filter(
|
let queries = searchQueries.filter(
|
||||||
(query) => query.model in searchQuery.data
|
(query) => (query.searchKey ?? query.model) in searchQuery.data
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const key in searchQuery.data) {
|
for (const key in searchQuery.data) {
|
||||||
const query = queries.find((q) => q.model == key);
|
const query = queries.find((q) => (q.searchKey ?? q.model) == key);
|
||||||
if (query) {
|
if (query) {
|
||||||
query.results = searchQuery.data[key];
|
query.results = searchQuery.data[key];
|
||||||
}
|
}
|
||||||
@ -402,7 +484,7 @@ export function SearchDrawer({
|
|||||||
<IconRefresh />
|
<IconRefresh />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Menu>
|
<Menu position='bottom-end'>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<Tooltip label={t`Search Options`} position='bottom-end'>
|
<Tooltip label={t`Search Options`} position='bottom-end'>
|
||||||
<ActionIcon size='lg' variant='transparent'>
|
<ActionIcon size='lg' variant='transparent'>
|
||||||
@ -443,16 +525,24 @@ export function SearchDrawer({
|
|||||||
)}
|
)}
|
||||||
{!searchQuery.isFetching && !searchQuery.isError && (
|
{!searchQuery.isFetching && !searchQuery.isError && (
|
||||||
<Stack gap='md'>
|
<Stack gap='md'>
|
||||||
{queryResults.map((query, idx) => (
|
<Accordion
|
||||||
<QueryResultGroup
|
multiple
|
||||||
key={idx}
|
defaultValue={searchQueries.map((q) => q.model)}
|
||||||
query={query}
|
>
|
||||||
onRemove={(query) => removeResults(query)}
|
{queryResults.map((query, idx) => (
|
||||||
onResultClick={(query, pk, event) =>
|
<QueryResultGroup
|
||||||
onResultClick(query, pk, event)
|
key={idx}
|
||||||
}
|
searchText={searchText}
|
||||||
/>
|
query={query}
|
||||||
))}
|
navigate={navigate}
|
||||||
|
onClose={closeDrawer}
|
||||||
|
onRemove={(query) => removeResults(query)}
|
||||||
|
onResultClick={(query, pk, event) =>
|
||||||
|
onResultClick(query, pk, event)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Accordion>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
{searchQuery.isError && (
|
{searchQuery.isError && (
|
||||||
|
@ -37,7 +37,6 @@ export const ModelInformationDict: ModelDict = {
|
|||||||
partparametertemplate: {
|
partparametertemplate: {
|
||||||
label: () => t`Part Parameter Template`,
|
label: () => t`Part Parameter Template`,
|
||||||
label_multiple: () => t`Part Parameter Templates`,
|
label_multiple: () => t`Part Parameter Templates`,
|
||||||
url_overview: '/partparametertemplate',
|
|
||||||
url_detail: '/partparametertemplate/:pk/',
|
url_detail: '/partparametertemplate/:pk/',
|
||||||
api_endpoint: ApiEndpoints.part_parameter_template_list,
|
api_endpoint: ApiEndpoints.part_parameter_template_list,
|
||||||
icon: 'test_templates'
|
icon: 'test_templates'
|
||||||
@ -45,7 +44,6 @@ export const ModelInformationDict: ModelDict = {
|
|||||||
parttesttemplate: {
|
parttesttemplate: {
|
||||||
label: () => t`Part Test Template`,
|
label: () => t`Part Test Template`,
|
||||||
label_multiple: () => t`Part Test Templates`,
|
label_multiple: () => t`Part Test Templates`,
|
||||||
url_overview: '/parttesttemplate',
|
|
||||||
url_detail: '/parttesttemplate/:pk/',
|
url_detail: '/parttesttemplate/:pk/',
|
||||||
api_endpoint: ApiEndpoints.part_test_template_list,
|
api_endpoint: ApiEndpoints.part_test_template_list,
|
||||||
icon: 'test'
|
icon: 'test'
|
||||||
@ -53,7 +51,7 @@ export const ModelInformationDict: ModelDict = {
|
|||||||
supplierpart: {
|
supplierpart: {
|
||||||
label: () => t`Supplier Part`,
|
label: () => t`Supplier Part`,
|
||||||
label_multiple: () => t`Supplier Parts`,
|
label_multiple: () => t`Supplier Parts`,
|
||||||
url_overview: '/supplierpart',
|
url_overview: '/purchasing/index/supplier-parts',
|
||||||
url_detail: '/purchasing/supplier-part/:pk/',
|
url_detail: '/purchasing/supplier-part/:pk/',
|
||||||
api_endpoint: ApiEndpoints.supplier_part_list,
|
api_endpoint: ApiEndpoints.supplier_part_list,
|
||||||
admin_url: '/company/supplierpart/',
|
admin_url: '/company/supplierpart/',
|
||||||
@ -62,7 +60,7 @@ export const ModelInformationDict: ModelDict = {
|
|||||||
manufacturerpart: {
|
manufacturerpart: {
|
||||||
label: () => t`Manufacturer Part`,
|
label: () => t`Manufacturer Part`,
|
||||||
label_multiple: () => t`Manufacturer Parts`,
|
label_multiple: () => t`Manufacturer Parts`,
|
||||||
url_overview: '/manufacturerpart',
|
url_overview: '/purchasing/index/manufacturer-parts',
|
||||||
url_detail: '/purchasing/manufacturer-part/:pk/',
|
url_detail: '/purchasing/manufacturer-part/:pk/',
|
||||||
api_endpoint: ApiEndpoints.manufacturer_part_list,
|
api_endpoint: ApiEndpoints.manufacturer_part_list,
|
||||||
admin_url: '/company/manufacturerpart/',
|
admin_url: '/company/manufacturerpart/',
|
||||||
@ -133,7 +131,6 @@ export const ModelInformationDict: ModelDict = {
|
|||||||
company: {
|
company: {
|
||||||
label: () => t`Company`,
|
label: () => t`Company`,
|
||||||
label_multiple: () => t`Companies`,
|
label_multiple: () => t`Companies`,
|
||||||
url_overview: '/company',
|
|
||||||
url_detail: '/company/:pk/',
|
url_detail: '/company/:pk/',
|
||||||
api_endpoint: ApiEndpoints.company_list,
|
api_endpoint: ApiEndpoints.company_list,
|
||||||
admin_url: '/company/company/',
|
admin_url: '/company/company/',
|
||||||
@ -142,7 +139,6 @@ export const ModelInformationDict: ModelDict = {
|
|||||||
projectcode: {
|
projectcode: {
|
||||||
label: () => t`Project Code`,
|
label: () => t`Project Code`,
|
||||||
label_multiple: () => t`Project Codes`,
|
label_multiple: () => t`Project Codes`,
|
||||||
url_overview: '/project-code',
|
|
||||||
url_detail: '/project-code/:pk/',
|
url_detail: '/project-code/:pk/',
|
||||||
api_endpoint: ApiEndpoints.project_code_list,
|
api_endpoint: ApiEndpoints.project_code_list,
|
||||||
icon: 'list_details'
|
icon: 'list_details'
|
||||||
@ -174,7 +170,6 @@ export const ModelInformationDict: ModelDict = {
|
|||||||
salesordershipment: {
|
salesordershipment: {
|
||||||
label: () => t`Sales Order Shipment`,
|
label: () => t`Sales Order Shipment`,
|
||||||
label_multiple: () => t`Sales Order Shipments`,
|
label_multiple: () => t`Sales Order Shipments`,
|
||||||
url_overview: '/sales/shipment/',
|
|
||||||
url_detail: '/sales/shipment/:pk/',
|
url_detail: '/sales/shipment/:pk/',
|
||||||
api_endpoint: ApiEndpoints.sales_order_shipment_list,
|
api_endpoint: ApiEndpoints.sales_order_shipment_list,
|
||||||
icon: 'sales_orders'
|
icon: 'sales_orders'
|
||||||
@ -197,7 +192,6 @@ export const ModelInformationDict: ModelDict = {
|
|||||||
address: {
|
address: {
|
||||||
label: () => t`Address`,
|
label: () => t`Address`,
|
||||||
label_multiple: () => t`Addresses`,
|
label_multiple: () => t`Addresses`,
|
||||||
url_overview: '/address',
|
|
||||||
url_detail: '/address/:pk/',
|
url_detail: '/address/:pk/',
|
||||||
api_endpoint: ApiEndpoints.address_list,
|
api_endpoint: ApiEndpoints.address_list,
|
||||||
icon: 'address'
|
icon: 'address'
|
||||||
@ -205,7 +199,6 @@ export const ModelInformationDict: ModelDict = {
|
|||||||
contact: {
|
contact: {
|
||||||
label: () => t`Contact`,
|
label: () => t`Contact`,
|
||||||
label_multiple: () => t`Contacts`,
|
label_multiple: () => t`Contacts`,
|
||||||
url_overview: '/contact',
|
|
||||||
url_detail: '/contact/:pk/',
|
url_detail: '/contact/:pk/',
|
||||||
api_endpoint: ApiEndpoints.contact_list,
|
api_endpoint: ApiEndpoints.contact_list,
|
||||||
icon: 'group'
|
icon: 'group'
|
||||||
@ -213,7 +206,6 @@ export const ModelInformationDict: ModelDict = {
|
|||||||
owner: {
|
owner: {
|
||||||
label: () => t`Owner`,
|
label: () => t`Owner`,
|
||||||
label_multiple: () => t`Owners`,
|
label_multiple: () => t`Owners`,
|
||||||
url_overview: '/owner',
|
|
||||||
url_detail: '/owner/:pk/',
|
url_detail: '/owner/:pk/',
|
||||||
api_endpoint: ApiEndpoints.owner_list,
|
api_endpoint: ApiEndpoints.owner_list,
|
||||||
icon: 'group'
|
icon: 'group'
|
||||||
@ -221,7 +213,6 @@ export const ModelInformationDict: ModelDict = {
|
|||||||
user: {
|
user: {
|
||||||
label: () => t`User`,
|
label: () => t`User`,
|
||||||
label_multiple: () => t`Users`,
|
label_multiple: () => t`Users`,
|
||||||
url_overview: '/user',
|
|
||||||
url_detail: '/user/:pk/',
|
url_detail: '/user/:pk/',
|
||||||
api_endpoint: ApiEndpoints.user_list,
|
api_endpoint: ApiEndpoints.user_list,
|
||||||
icon: 'user'
|
icon: 'user'
|
||||||
@ -229,7 +220,6 @@ export const ModelInformationDict: ModelDict = {
|
|||||||
group: {
|
group: {
|
||||||
label: () => t`Group`,
|
label: () => t`Group`,
|
||||||
label_multiple: () => t`Groups`,
|
label_multiple: () => t`Groups`,
|
||||||
url_overview: '/user/group',
|
|
||||||
url_detail: '/user/group-:pk',
|
url_detail: '/user/group-:pk',
|
||||||
api_endpoint: ApiEndpoints.group_list,
|
api_endpoint: ApiEndpoints.group_list,
|
||||||
admin_url: '/auth/group/',
|
admin_url: '/auth/group/',
|
||||||
@ -238,7 +228,7 @@ export const ModelInformationDict: ModelDict = {
|
|||||||
importsession: {
|
importsession: {
|
||||||
label: () => t`Import Session`,
|
label: () => t`Import Session`,
|
||||||
label_multiple: () => t`Import Sessions`,
|
label_multiple: () => t`Import Sessions`,
|
||||||
url_overview: '/import',
|
url_overview: '/settings/admin/import',
|
||||||
url_detail: '/import/:pk/',
|
url_detail: '/import/:pk/',
|
||||||
api_endpoint: ApiEndpoints.import_session_list,
|
api_endpoint: ApiEndpoints.import_session_list,
|
||||||
icon: 'import'
|
icon: 'import'
|
||||||
@ -246,24 +236,24 @@ export const ModelInformationDict: ModelDict = {
|
|||||||
labeltemplate: {
|
labeltemplate: {
|
||||||
label: () => t`Label Template`,
|
label: () => t`Label Template`,
|
||||||
label_multiple: () => t`Label Templates`,
|
label_multiple: () => t`Label Templates`,
|
||||||
url_overview: '/labeltemplate',
|
url_overview: '/settings/admin/labels',
|
||||||
url_detail: '/labeltemplate/:pk/',
|
url_detail: '/settings/admin/labels/:pk/',
|
||||||
api_endpoint: ApiEndpoints.label_list,
|
api_endpoint: ApiEndpoints.label_list,
|
||||||
icon: 'labels'
|
icon: 'labels'
|
||||||
},
|
},
|
||||||
reporttemplate: {
|
reporttemplate: {
|
||||||
label: () => t`Report Template`,
|
label: () => t`Report Template`,
|
||||||
label_multiple: () => t`Report Templates`,
|
label_multiple: () => t`Report Templates`,
|
||||||
url_overview: '/reporttemplate',
|
url_overview: '/settings/admin/reports',
|
||||||
url_detail: '/reporttemplate/:pk/',
|
url_detail: '/settings/admin/reports/:pk/',
|
||||||
api_endpoint: ApiEndpoints.report_list,
|
api_endpoint: ApiEndpoints.report_list,
|
||||||
icon: 'reports'
|
icon: 'reports'
|
||||||
},
|
},
|
||||||
pluginconfig: {
|
pluginconfig: {
|
||||||
label: () => t`Plugin Configuration`,
|
label: () => t`Plugin Configuration`,
|
||||||
label_multiple: () => t`Plugin Configurations`,
|
label_multiple: () => t`Plugin Configurations`,
|
||||||
url_overview: '/pluginconfig',
|
url_overview: '/settings/admin/plugin',
|
||||||
url_detail: '/pluginconfig/:pk/',
|
url_detail: '/settings/admin/plugin/:pk/',
|
||||||
api_endpoint: ApiEndpoints.plugin_list,
|
api_endpoint: ApiEndpoints.plugin_list,
|
||||||
icon: 'plugin'
|
icon: 'plugin'
|
||||||
},
|
},
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Grid, Skeleton, Stack } from '@mantine/core';
|
import { Grid, Skeleton, Stack } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconBuildingFactory2,
|
|
||||||
IconBuildingWarehouse,
|
IconBuildingWarehouse,
|
||||||
IconInfoCircle,
|
IconInfoCircle,
|
||||||
IconMap2,
|
IconMap2,
|
||||||
@ -176,24 +175,24 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
|
|||||||
icon: <IconInfoCircle />,
|
icon: <IconInfoCircle />,
|
||||||
content: detailsPanel
|
content: detailsPanel
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'manufactured-parts',
|
|
||||||
label: t`Manufactured Parts`,
|
|
||||||
icon: <IconBuildingFactory2 />,
|
|
||||||
hidden: !company?.is_manufacturer,
|
|
||||||
content: company?.pk && (
|
|
||||||
<ManufacturerPartTable params={{ manufacturer: company.pk }} />
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'supplied-parts',
|
name: 'supplied-parts',
|
||||||
label: t`Supplied Parts`,
|
label: t`Supplied Parts`,
|
||||||
icon: <IconBuildingWarehouse />,
|
icon: <IconPackageExport />,
|
||||||
hidden: !company?.is_supplier,
|
hidden: !company?.is_supplier,
|
||||||
content: company?.pk && (
|
content: company?.pk && (
|
||||||
<SupplierPartTable params={{ supplier: company.pk }} />
|
<SupplierPartTable params={{ supplier: company.pk }} />
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'manufactured-parts',
|
||||||
|
label: t`Manufactured Parts`,
|
||||||
|
icon: <IconBuildingWarehouse />,
|
||||||
|
hidden: !company?.is_manufacturer,
|
||||||
|
content: company?.pk && (
|
||||||
|
<ManufacturerPartTable params={{ manufacturer: company.pk }} />
|
||||||
|
)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'purchase-orders',
|
name: 'purchase-orders',
|
||||||
label: t`Purchase Orders`,
|
label: t`Purchase Orders`,
|
||||||
|
@ -3,6 +3,8 @@ import { Stack } from '@mantine/core';
|
|||||||
import {
|
import {
|
||||||
IconBuildingFactory2,
|
IconBuildingFactory2,
|
||||||
IconBuildingStore,
|
IconBuildingStore,
|
||||||
|
IconBuildingWarehouse,
|
||||||
|
IconPackageExport,
|
||||||
IconShoppingCart
|
IconShoppingCart
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
@ -13,7 +15,9 @@ import { PanelGroup } from '../../components/panels/PanelGroup';
|
|||||||
import { UserRoles } from '../../enums/Roles';
|
import { UserRoles } from '../../enums/Roles';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import { CompanyTable } from '../../tables/company/CompanyTable';
|
import { CompanyTable } from '../../tables/company/CompanyTable';
|
||||||
|
import { ManufacturerPartTable } from '../../tables/purchasing/ManufacturerPartTable';
|
||||||
import { PurchaseOrderTable } from '../../tables/purchasing/PurchaseOrderTable';
|
import { PurchaseOrderTable } from '../../tables/purchasing/PurchaseOrderTable';
|
||||||
|
import { SupplierPartTable } from '../../tables/purchasing/SupplierPartTable';
|
||||||
|
|
||||||
export default function PurchasingIndex() {
|
export default function PurchasingIndex() {
|
||||||
const user = useUserState();
|
const user = useUserState();
|
||||||
@ -38,6 +42,12 @@ export default function PurchasingIndex() {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'supplier-parts',
|
||||||
|
label: t`Supplier Parts`,
|
||||||
|
icon: <IconPackageExport />,
|
||||||
|
content: <SupplierPartTable params={{}} />
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'manufacturer',
|
name: 'manufacturer',
|
||||||
label: t`Manufacturers`,
|
label: t`Manufacturers`,
|
||||||
@ -48,6 +58,12 @@ export default function PurchasingIndex() {
|
|||||||
params={{ is_manufacturer: true }}
|
params={{ is_manufacturer: true }}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'manufacturer-parts',
|
||||||
|
label: t`Manufacturer Parts`,
|
||||||
|
icon: <IconBuildingWarehouse />,
|
||||||
|
content: <ManufacturerPartTable params={{}} />
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
@ -36,7 +36,7 @@ export default function PurchasingIndex() {
|
|||||||
hidden: !user.hasViewRole(UserRoles.return_order)
|
hidden: !user.hasViewRole(UserRoles.return_order)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'suppliers',
|
name: 'customers',
|
||||||
label: t`Customers`,
|
label: t`Customers`,
|
||||||
icon: <IconBuildingStore />,
|
icon: <IconBuildingStore />,
|
||||||
content: (
|
content: (
|
||||||
|
@ -13,7 +13,7 @@ import {
|
|||||||
IconRefresh,
|
IconRefresh,
|
||||||
IconTrash
|
IconTrash
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Fragment } from 'react/jsx-runtime';
|
import { Fragment } from 'react/jsx-runtime';
|
||||||
|
|
||||||
import { Boundary } from '../components/Boundary';
|
import { Boundary } from '../components/Boundary';
|
||||||
@ -122,6 +122,18 @@ export default function InvenTreeTableHeader({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const hasCustomSearch = useMemo(() => {
|
||||||
|
return tableState.queryFilters.has('search');
|
||||||
|
}, [tableState.queryFilters]);
|
||||||
|
|
||||||
|
const hasCustomFilters = useMemo(() => {
|
||||||
|
if (hasCustomSearch) {
|
||||||
|
return tableState.queryFilters.size > 1;
|
||||||
|
} else {
|
||||||
|
return tableState.queryFilters.size > 0;
|
||||||
|
}
|
||||||
|
}, [hasCustomSearch, tableState.queryFilters]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{deleteRecords.modal}
|
{deleteRecords.modal}
|
||||||
@ -135,7 +147,7 @@ export default function InvenTreeTableHeader({
|
|||||||
/>
|
/>
|
||||||
</Boundary>
|
</Boundary>
|
||||||
)}
|
)}
|
||||||
{tableState.queryFilters.size > 0 && (
|
{(hasCustomFilters || hasCustomSearch) && (
|
||||||
<Alert
|
<Alert
|
||||||
color='yellow'
|
color='yellow'
|
||||||
withCloseButton
|
withCloseButton
|
||||||
@ -143,7 +155,6 @@ export default function InvenTreeTableHeader({
|
|||||||
onClose={() => tableState.clearQueryFilters()}
|
onClose={() => tableState.clearQueryFilters()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Group justify='apart' grow wrap='nowrap'>
|
<Group justify='apart' grow wrap='nowrap'>
|
||||||
<Group justify='left' key='custom-actions' gap={5} wrap='nowrap'>
|
<Group justify='left' key='custom-actions' gap={5} wrap='nowrap'>
|
||||||
<PrintingActions
|
<PrintingActions
|
||||||
@ -180,6 +191,7 @@ export default function InvenTreeTableHeader({
|
|||||||
<Group justify='right' gap={5} wrap='nowrap'>
|
<Group justify='right' gap={5} wrap='nowrap'>
|
||||||
{tableProps.enableSearch && (
|
{tableProps.enableSearch && (
|
||||||
<TableSearchInput
|
<TableSearchInput
|
||||||
|
disabled={hasCustomSearch}
|
||||||
searchCallback={(term: string) => tableState.setSearchTerm(term)}
|
searchCallback={(term: string) => tableState.setSearchTerm(term)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -208,6 +220,7 @@ export default function InvenTreeTableHeader({
|
|||||||
disabled={tableState.activeFilters?.length == 0}
|
disabled={tableState.activeFilters?.length == 0}
|
||||||
>
|
>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
|
disabled={hasCustomFilters}
|
||||||
variant='transparent'
|
variant='transparent'
|
||||||
aria-label='table-select-filters'
|
aria-label='table-select-filters'
|
||||||
>
|
>
|
||||||
|
@ -5,8 +5,10 @@ import { IconSearch } from '@tabler/icons-react';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export function TableSearchInput({
|
export function TableSearchInput({
|
||||||
|
disabled,
|
||||||
searchCallback
|
searchCallback
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
|
disabled?: boolean;
|
||||||
searchCallback: (searchTerm: string) => void;
|
searchCallback: (searchTerm: string) => void;
|
||||||
}>) {
|
}>) {
|
||||||
const [value, setValue] = useState<string>('');
|
const [value, setValue] = useState<string>('');
|
||||||
@ -19,6 +21,7 @@ export function TableSearchInput({
|
|||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
value={value}
|
value={value}
|
||||||
|
disabled={disabled}
|
||||||
leftSection={<IconSearch />}
|
leftSection={<IconSearch />}
|
||||||
placeholder={t`Search`}
|
placeholder={t`Search`}
|
||||||
onChange={(event) => setValue(event.target.value)}
|
onChange={(event) => setValue(event.target.value)}
|
||||||
|
@ -73,7 +73,7 @@ export function SupplierPartTable({
|
|||||||
DescriptionColumn({}),
|
DescriptionColumn({}),
|
||||||
{
|
{
|
||||||
accessor: 'manufacturer',
|
accessor: 'manufacturer',
|
||||||
|
title: t`Manufacturer`,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
render: (record: any) => {
|
render: (record: any) => {
|
||||||
const manufacturer = record?.manufacturer_detail ?? {};
|
const manufacturer = record?.manufacturer_detail ?? {};
|
||||||
|
@ -97,4 +97,5 @@ export const globalSearch = async (page, query) => {
|
|||||||
await page.getByLabel('global-search-input').clear();
|
await page.getByLabel('global-search-input').clear();
|
||||||
await page.getByPlaceholder('Enter search text').fill(query);
|
await page.getByPlaceholder('Enter search text').fill(query);
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
};
|
};
|
||||||
|
40
src/frontend/tests/pages/pui_company.spec.ts
Normal file
40
src/frontend/tests/pages/pui_company.spec.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { test } from '../baseFixtures.js';
|
||||||
|
import { navigate } from '../helpers.js';
|
||||||
|
import { doQuickLogin } from '../login.js';
|
||||||
|
|
||||||
|
test('Company', async ({ page }) => {
|
||||||
|
await doQuickLogin(page);
|
||||||
|
|
||||||
|
await navigate(page, 'company/1/details');
|
||||||
|
await page.getByLabel('Details').getByText('DigiKey Electronics').waitFor();
|
||||||
|
await page.getByRole('cell', { name: 'https://www.digikey.com/' }).waitFor();
|
||||||
|
await page.getByRole('tab', { name: 'Supplied Parts' }).click();
|
||||||
|
await page
|
||||||
|
.getByRole('cell', { name: 'RR05P100KDTR-ND', exact: true })
|
||||||
|
.waitFor();
|
||||||
|
await page.getByRole('tab', { name: 'Purchase Orders' }).click();
|
||||||
|
await page.getByRole('cell', { name: 'Molex connectors' }).first().waitFor();
|
||||||
|
await page.getByRole('tab', { name: 'Stock Items' }).click();
|
||||||
|
await page
|
||||||
|
.getByRole('cell', { name: 'Blue plastic enclosure' })
|
||||||
|
.first()
|
||||||
|
.waitFor();
|
||||||
|
await page.getByRole('tab', { name: 'Contacts' }).click();
|
||||||
|
await page.getByRole('cell', { name: 'jimmy.mcleod@digikey.com' }).waitFor();
|
||||||
|
await page.getByRole('tab', { name: 'Addresses' }).click();
|
||||||
|
await page.getByRole('cell', { name: 'Carla Tunnel' }).waitFor();
|
||||||
|
await page.getByRole('tab', { name: 'Attachments' }).click();
|
||||||
|
await page.getByRole('tab', { name: 'Notes' }).click();
|
||||||
|
|
||||||
|
// Let's edit the company details
|
||||||
|
await page.getByLabel('action-menu-company-actions').click();
|
||||||
|
await page.getByLabel('action-menu-company-actions-edit').click();
|
||||||
|
|
||||||
|
await page.getByLabel('text-field-name').fill('');
|
||||||
|
await page.getByLabel('text-field-website').fill('invalid-website');
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
|
||||||
|
await page.getByText('This field may not be blank.').waitFor();
|
||||||
|
await page.getByText('Enter a valid URL.').waitFor();
|
||||||
|
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||||
|
});
|
@ -1,44 +1,7 @@
|
|||||||
import { test } from './baseFixtures.js';
|
import { test } from './baseFixtures.js';
|
||||||
import { navigate } from './helpers.js';
|
import { globalSearch, navigate } from './helpers.js';
|
||||||
import { doQuickLogin } from './login.js';
|
import { doQuickLogin } from './login.js';
|
||||||
|
|
||||||
test('Company', async ({ page }) => {
|
|
||||||
await doQuickLogin(page);
|
|
||||||
|
|
||||||
await navigate(page, 'company/1/details');
|
|
||||||
await page.getByLabel('Details').getByText('DigiKey Electronics').waitFor();
|
|
||||||
await page.getByRole('cell', { name: 'https://www.digikey.com/' }).waitFor();
|
|
||||||
await page.getByRole('tab', { name: 'Supplied Parts' }).click();
|
|
||||||
await page
|
|
||||||
.getByRole('cell', { name: 'RR05P100KDTR-ND', exact: true })
|
|
||||||
.waitFor();
|
|
||||||
await page.getByRole('tab', { name: 'Purchase Orders' }).click();
|
|
||||||
await page.getByRole('cell', { name: 'Molex connectors' }).first().waitFor();
|
|
||||||
await page.getByRole('tab', { name: 'Stock Items' }).click();
|
|
||||||
await page
|
|
||||||
.getByRole('cell', { name: 'Blue plastic enclosure' })
|
|
||||||
.first()
|
|
||||||
.waitFor();
|
|
||||||
await page.getByRole('tab', { name: 'Contacts' }).click();
|
|
||||||
await page.getByRole('cell', { name: 'jimmy.mcleod@digikey.com' }).waitFor();
|
|
||||||
await page.getByRole('tab', { name: 'Addresses' }).click();
|
|
||||||
await page.getByRole('cell', { name: 'Carla Tunnel' }).waitFor();
|
|
||||||
await page.getByRole('tab', { name: 'Attachments' }).click();
|
|
||||||
await page.getByRole('tab', { name: 'Notes' }).click();
|
|
||||||
|
|
||||||
// Let's edit the company details
|
|
||||||
await page.getByLabel('action-menu-company-actions').click();
|
|
||||||
await page.getByLabel('action-menu-company-actions-edit').click();
|
|
||||||
|
|
||||||
await page.getByLabel('text-field-name').fill('');
|
|
||||||
await page.getByLabel('text-field-website').fill('invalid-website');
|
|
||||||
await page.getByRole('button', { name: 'Submit' }).click();
|
|
||||||
|
|
||||||
await page.getByText('This field may not be blank.').waitFor();
|
|
||||||
await page.getByText('Enter a valid URL.').waitFor();
|
|
||||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test for integration of django admin button
|
* Test for integration of django admin button
|
||||||
*/
|
*/
|
||||||
@ -53,3 +16,39 @@ test('Admin Button', async ({ page }) => {
|
|||||||
await page.getByRole('heading', { name: 'Change Company' }).waitFor();
|
await page.getByRole('heading', { name: 'Change Company' }).waitFor();
|
||||||
await page.getByRole('link', { name: 'View on site' }).waitFor();
|
await page.getByRole('link', { name: 'View on site' }).waitFor();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Tests for the global search functionality
|
||||||
|
test('Search', async ({ page }) => {
|
||||||
|
await doQuickLogin(page, 'steven', 'wizardstaff');
|
||||||
|
|
||||||
|
await globalSearch(page, 'another customer');
|
||||||
|
|
||||||
|
// Check for expected results
|
||||||
|
await page.locator('a').filter({ hasText: 'Customer B' }).first().waitFor();
|
||||||
|
await page.locator('a').filter({ hasText: 'Customer C' }).first().waitFor();
|
||||||
|
await page.locator('a').filter({ hasText: 'Customer D' }).first().waitFor();
|
||||||
|
await page.locator('a').filter({ hasText: 'Customer E' }).first().waitFor();
|
||||||
|
|
||||||
|
// Click through to the "Customer" results
|
||||||
|
await page.getByRole('button', { name: 'view-all-results-customer' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('**/sales/index/customers**');
|
||||||
|
await page.getByText('Custom table filters are active').waitFor();
|
||||||
|
|
||||||
|
await globalSearch(page, '0402 res');
|
||||||
|
|
||||||
|
await page
|
||||||
|
.locator('span')
|
||||||
|
.filter({ hasText: 'Parts - 16 results' })
|
||||||
|
.first()
|
||||||
|
.waitFor();
|
||||||
|
await page
|
||||||
|
.locator('span')
|
||||||
|
.filter({ hasText: 'Supplier Parts - 138 results' })
|
||||||
|
.first()
|
||||||
|
.waitFor();
|
||||||
|
|
||||||
|
await page.getByLabel('view-all-results-manufacturerpart').click();
|
||||||
|
await page.waitForURL('**/purchasing/index/manufacturer-parts**');
|
||||||
|
await page.getByRole('cell', { name: 'RT0402BRD07100KL' }).waitFor();
|
||||||
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user