2
0
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:
Oliver 2025-02-22 15:00:25 +11:00 committed by GitHub
parent 09cdf72bda
commit 44cca7ddf2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 290 additions and 121 deletions

View File

@ -522,6 +522,9 @@ class APISearchView(GenericAPIView):
return {
'build': build.api.BuildList,
'company': company.api.CompanyList,
'supplier': company.api.CompanyList,
'manufacturer': company.api.CompanyList,
'customer': company.api.CompanyList,
'manufacturerpart': company.api.ManufacturerPartList,
'supplierpart': company.api.SupplierPartList,
'part': part.api.PartList,
@ -534,6 +537,14 @@ class APISearchView(GenericAPIView):
'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):
"""Perform search query against available models."""
data = request.data
@ -552,6 +563,8 @@ class APISearchView(GenericAPIView):
if 'search' not in data:
raise ValidationError({'search': 'Search term must be provided'})
search_filters = self.get_result_filters()
for key, cls in self.get_result_types().items():
# Only return results which are specifically requested
if key in data:
@ -560,6 +573,11 @@ class APISearchView(GenericAPIView):
for k, v in pass_through_params.items():
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
params['format'] = 'json'

View File

@ -1,16 +1,15 @@
import { Trans, t } from '@lingui/macro';
import {
Accordion,
ActionIcon,
Alert,
Anchor,
Center,
Checkbox,
Divider,
Drawer,
Group,
Loader,
Menu,
Paper,
Space,
Stack,
Text,
@ -21,19 +20,23 @@ import { useDebouncedValue } from '@mantine/hooks';
import {
IconAlertCircle,
IconBackspace,
IconExclamationCircle,
IconRefresh,
IconSearch,
IconSettings,
IconTableExport,
IconX
} from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { type NavigateFunction, useNavigate } from 'react-router-dom';
import { showNotification } from '@mantine/notifications';
import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { cancelEvent } from '../../functions/events';
import { navigateToLink } from '../../functions/navigation';
import { apiUrl } from '../../states/ApiState';
import { useUserSettingsState } from '../../states/SettingsState';
@ -45,6 +48,9 @@ import { ModelInformationDict, getModelInfo } from '../render/ModelType';
// Define type for handling individual search queries
type SearchQuery = {
model: ModelType;
searchKey?: string;
title?: string;
overviewUrl?: string;
enabled: boolean;
parameters: any;
results?: any;
@ -54,38 +60,86 @@ type SearchQuery = {
* Render the results for a single search query
*/
function QueryResultGroup({
searchText,
query,
navigate,
onClose,
onRemove,
onResultClick
}: Readonly<{
searchText: string;
query: SearchQuery;
navigate: NavigateFunction;
onClose: () => void;
onRemove: (query: ModelType) => 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) {
return null;
}
const model = getModelInfo(query.model);
return (
<Paper
withBorder
shadow='sm'
p='md'
key={`paper-${query.model}`}
aria-label={`search-group-${query.model}`}
>
<Stack key={`stack-${query.model}`}>
<Group justify='space-between' wrap='nowrap'>
<Accordion.Item key={query.model} value={query.model}>
<Accordion.Control component='div'>
<Group justify='space-between'>
<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' }}>
{' '}
- {query.results.count} <Trans>results</Trans>
</Text>
</Group>
<Space />
<Group justify='right' wrap='nowrap'>
<Tooltip label={t`Remove search group`} position='top-end'>
<ActionIcon
size='sm'
color='red'
@ -96,8 +150,12 @@ function QueryResultGroup({
>
<IconX />
</ActionIcon>
</Tooltip>
<Space />
</Group>
<Divider />
</Group>
</Accordion.Control>
<Accordion.Panel>
<Stack aria-label={`search-group-results-${query.model}`}>
{query.results.results.map((result: any) => (
<Anchor
@ -111,9 +169,8 @@ function QueryResultGroup({
</Anchor>
))}
</Stack>
<Space />
</Stack>
</Paper>
</Accordion.Panel>
</Accordion.Item>
);
}
@ -213,10 +270,32 @@ export function SearchDrawer({
},
{
model: ModelType.company,
overviewUrl: '/purchasing/index/suppliers',
searchKey: 'supplier',
title: t`Suppliers`,
parameters: {},
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')
},
{
@ -272,7 +351,9 @@ export function SearchDrawer({
}, [user, userSettings]);
// 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
useEffect(() => {
@ -296,7 +377,8 @@ export function SearchDrawer({
// Add in custom query parameters
searchQueries.forEach((query) => {
params[query.model] = query.parameters;
const key = query.searchKey || query.model;
params[key] = query.parameters;
});
return api
@ -321,11 +403,11 @@ export function SearchDrawer({
useEffect(() => {
if (searchQuery.data) {
let queries = searchQueries.filter(
(query) => query.model in searchQuery.data
(query) => (query.searchKey ?? query.model) 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) {
query.results = searchQuery.data[key];
}
@ -402,7 +484,7 @@ export function SearchDrawer({
<IconRefresh />
</ActionIcon>
</Tooltip>
<Menu>
<Menu position='bottom-end'>
<Menu.Target>
<Tooltip label={t`Search Options`} position='bottom-end'>
<ActionIcon size='lg' variant='transparent'>
@ -443,16 +525,24 @@ export function SearchDrawer({
)}
{!searchQuery.isFetching && !searchQuery.isError && (
<Stack gap='md'>
<Accordion
multiple
defaultValue={searchQueries.map((q) => q.model)}
>
{queryResults.map((query, idx) => (
<QueryResultGroup
key={idx}
searchText={searchText}
query={query}
navigate={navigate}
onClose={closeDrawer}
onRemove={(query) => removeResults(query)}
onResultClick={(query, pk, event) =>
onResultClick(query, pk, event)
}
/>
))}
</Accordion>
</Stack>
)}
{searchQuery.isError && (

View File

@ -37,7 +37,6 @@ export const ModelInformationDict: ModelDict = {
partparametertemplate: {
label: () => t`Part Parameter Template`,
label_multiple: () => t`Part Parameter Templates`,
url_overview: '/partparametertemplate',
url_detail: '/partparametertemplate/:pk/',
api_endpoint: ApiEndpoints.part_parameter_template_list,
icon: 'test_templates'
@ -45,7 +44,6 @@ export const ModelInformationDict: ModelDict = {
parttesttemplate: {
label: () => t`Part Test Template`,
label_multiple: () => t`Part Test Templates`,
url_overview: '/parttesttemplate',
url_detail: '/parttesttemplate/:pk/',
api_endpoint: ApiEndpoints.part_test_template_list,
icon: 'test'
@ -53,7 +51,7 @@ export const ModelInformationDict: ModelDict = {
supplierpart: {
label: () => t`Supplier Part`,
label_multiple: () => t`Supplier Parts`,
url_overview: '/supplierpart',
url_overview: '/purchasing/index/supplier-parts',
url_detail: '/purchasing/supplier-part/:pk/',
api_endpoint: ApiEndpoints.supplier_part_list,
admin_url: '/company/supplierpart/',
@ -62,7 +60,7 @@ export const ModelInformationDict: ModelDict = {
manufacturerpart: {
label: () => t`Manufacturer Part`,
label_multiple: () => t`Manufacturer Parts`,
url_overview: '/manufacturerpart',
url_overview: '/purchasing/index/manufacturer-parts',
url_detail: '/purchasing/manufacturer-part/:pk/',
api_endpoint: ApiEndpoints.manufacturer_part_list,
admin_url: '/company/manufacturerpart/',
@ -133,7 +131,6 @@ export const ModelInformationDict: ModelDict = {
company: {
label: () => t`Company`,
label_multiple: () => t`Companies`,
url_overview: '/company',
url_detail: '/company/:pk/',
api_endpoint: ApiEndpoints.company_list,
admin_url: '/company/company/',
@ -142,7 +139,6 @@ export const ModelInformationDict: ModelDict = {
projectcode: {
label: () => t`Project Code`,
label_multiple: () => t`Project Codes`,
url_overview: '/project-code',
url_detail: '/project-code/:pk/',
api_endpoint: ApiEndpoints.project_code_list,
icon: 'list_details'
@ -174,7 +170,6 @@ export const ModelInformationDict: ModelDict = {
salesordershipment: {
label: () => t`Sales Order Shipment`,
label_multiple: () => t`Sales Order Shipments`,
url_overview: '/sales/shipment/',
url_detail: '/sales/shipment/:pk/',
api_endpoint: ApiEndpoints.sales_order_shipment_list,
icon: 'sales_orders'
@ -197,7 +192,6 @@ export const ModelInformationDict: ModelDict = {
address: {
label: () => t`Address`,
label_multiple: () => t`Addresses`,
url_overview: '/address',
url_detail: '/address/:pk/',
api_endpoint: ApiEndpoints.address_list,
icon: 'address'
@ -205,7 +199,6 @@ export const ModelInformationDict: ModelDict = {
contact: {
label: () => t`Contact`,
label_multiple: () => t`Contacts`,
url_overview: '/contact',
url_detail: '/contact/:pk/',
api_endpoint: ApiEndpoints.contact_list,
icon: 'group'
@ -213,7 +206,6 @@ export const ModelInformationDict: ModelDict = {
owner: {
label: () => t`Owner`,
label_multiple: () => t`Owners`,
url_overview: '/owner',
url_detail: '/owner/:pk/',
api_endpoint: ApiEndpoints.owner_list,
icon: 'group'
@ -221,7 +213,6 @@ export const ModelInformationDict: ModelDict = {
user: {
label: () => t`User`,
label_multiple: () => t`Users`,
url_overview: '/user',
url_detail: '/user/:pk/',
api_endpoint: ApiEndpoints.user_list,
icon: 'user'
@ -229,7 +220,6 @@ export const ModelInformationDict: ModelDict = {
group: {
label: () => t`Group`,
label_multiple: () => t`Groups`,
url_overview: '/user/group',
url_detail: '/user/group-:pk',
api_endpoint: ApiEndpoints.group_list,
admin_url: '/auth/group/',
@ -238,7 +228,7 @@ export const ModelInformationDict: ModelDict = {
importsession: {
label: () => t`Import Session`,
label_multiple: () => t`Import Sessions`,
url_overview: '/import',
url_overview: '/settings/admin/import',
url_detail: '/import/:pk/',
api_endpoint: ApiEndpoints.import_session_list,
icon: 'import'
@ -246,24 +236,24 @@ export const ModelInformationDict: ModelDict = {
labeltemplate: {
label: () => t`Label Template`,
label_multiple: () => t`Label Templates`,
url_overview: '/labeltemplate',
url_detail: '/labeltemplate/:pk/',
url_overview: '/settings/admin/labels',
url_detail: '/settings/admin/labels/:pk/',
api_endpoint: ApiEndpoints.label_list,
icon: 'labels'
},
reporttemplate: {
label: () => t`Report Template`,
label_multiple: () => t`Report Templates`,
url_overview: '/reporttemplate',
url_detail: '/reporttemplate/:pk/',
url_overview: '/settings/admin/reports',
url_detail: '/settings/admin/reports/:pk/',
api_endpoint: ApiEndpoints.report_list,
icon: 'reports'
},
pluginconfig: {
label: () => t`Plugin Configuration`,
label_multiple: () => t`Plugin Configurations`,
url_overview: '/pluginconfig',
url_detail: '/pluginconfig/:pk/',
url_overview: '/settings/admin/plugin',
url_detail: '/settings/admin/plugin/:pk/',
api_endpoint: ApiEndpoints.plugin_list,
icon: 'plugin'
},

View File

@ -1,7 +1,6 @@
import { t } from '@lingui/macro';
import { Grid, Skeleton, Stack } from '@mantine/core';
import {
IconBuildingFactory2,
IconBuildingWarehouse,
IconInfoCircle,
IconMap2,
@ -176,24 +175,24 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
icon: <IconInfoCircle />,
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',
label: t`Supplied Parts`,
icon: <IconBuildingWarehouse />,
icon: <IconPackageExport />,
hidden: !company?.is_supplier,
content: 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',
label: t`Purchase Orders`,

View File

@ -3,6 +3,8 @@ import { Stack } from '@mantine/core';
import {
IconBuildingFactory2,
IconBuildingStore,
IconBuildingWarehouse,
IconPackageExport,
IconShoppingCart
} from '@tabler/icons-react';
import { useMemo } from 'react';
@ -13,7 +15,9 @@ import { PanelGroup } from '../../components/panels/PanelGroup';
import { UserRoles } from '../../enums/Roles';
import { useUserState } from '../../states/UserState';
import { CompanyTable } from '../../tables/company/CompanyTable';
import { ManufacturerPartTable } from '../../tables/purchasing/ManufacturerPartTable';
import { PurchaseOrderTable } from '../../tables/purchasing/PurchaseOrderTable';
import { SupplierPartTable } from '../../tables/purchasing/SupplierPartTable';
export default function PurchasingIndex() {
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',
label: t`Manufacturers`,
@ -48,6 +58,12 @@ export default function PurchasingIndex() {
params={{ is_manufacturer: true }}
/>
)
},
{
name: 'manufacturer-parts',
label: t`Manufacturer Parts`,
icon: <IconBuildingWarehouse />,
content: <ManufacturerPartTable params={{}} />
}
];
}, [user]);

View File

@ -36,7 +36,7 @@ export default function PurchasingIndex() {
hidden: !user.hasViewRole(UserRoles.return_order)
},
{
name: 'suppliers',
name: 'customers',
label: t`Customers`,
icon: <IconBuildingStore />,
content: (

View File

@ -13,7 +13,7 @@ import {
IconRefresh,
IconTrash
} from '@tabler/icons-react';
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { Fragment } from 'react/jsx-runtime';
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 (
<>
{deleteRecords.modal}
@ -135,7 +147,7 @@ export default function InvenTreeTableHeader({
/>
</Boundary>
)}
{tableState.queryFilters.size > 0 && (
{(hasCustomFilters || hasCustomSearch) && (
<Alert
color='yellow'
withCloseButton
@ -143,7 +155,6 @@ export default function InvenTreeTableHeader({
onClose={() => tableState.clearQueryFilters()}
/>
)}
<Group justify='apart' grow wrap='nowrap'>
<Group justify='left' key='custom-actions' gap={5} wrap='nowrap'>
<PrintingActions
@ -180,6 +191,7 @@ export default function InvenTreeTableHeader({
<Group justify='right' gap={5} wrap='nowrap'>
{tableProps.enableSearch && (
<TableSearchInput
disabled={hasCustomSearch}
searchCallback={(term: string) => tableState.setSearchTerm(term)}
/>
)}
@ -208,6 +220,7 @@ export default function InvenTreeTableHeader({
disabled={tableState.activeFilters?.length == 0}
>
<ActionIcon
disabled={hasCustomFilters}
variant='transparent'
aria-label='table-select-filters'
>

View File

@ -5,8 +5,10 @@ import { IconSearch } from '@tabler/icons-react';
import { useEffect, useState } from 'react';
export function TableSearchInput({
disabled,
searchCallback
}: Readonly<{
disabled?: boolean;
searchCallback: (searchTerm: string) => void;
}>) {
const [value, setValue] = useState<string>('');
@ -19,6 +21,7 @@ export function TableSearchInput({
return (
<TextInput
value={value}
disabled={disabled}
leftSection={<IconSearch />}
placeholder={t`Search`}
onChange={(event) => setValue(event.target.value)}

View File

@ -73,7 +73,7 @@ export function SupplierPartTable({
DescriptionColumn({}),
{
accessor: 'manufacturer',
title: t`Manufacturer`,
sortable: true,
render: (record: any) => {
const manufacturer = record?.manufacturer_detail ?? {};

View File

@ -97,4 +97,5 @@ export const globalSearch = async (page, query) => {
await page.getByLabel('global-search-input').clear();
await page.getByPlaceholder('Enter search text').fill(query);
await page.waitForTimeout(300);
await page.waitForLoadState('networkidle');
};

View 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();
});

View File

@ -1,44 +1,7 @@
import { test } from './baseFixtures.js';
import { navigate } from './helpers.js';
import { globalSearch, 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();
});
/**
* 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('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();
});