From 44cca7ddf2b85bec98e2c9c30f5b23f7edbc8a37 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 22 Feb 2025 15:00:25 +1100 Subject: [PATCH] [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 --- src/backend/InvenTree/InvenTree/api.py | 18 ++ .../src/components/nav/SearchDrawer.tsx | 186 +++++++++++++----- .../src/components/render/ModelType.tsx | 28 +-- .../src/pages/company/CompanyDetail.tsx | 21 +- .../src/pages/purchasing/PurchasingIndex.tsx | 16 ++ src/frontend/src/pages/sales/SalesIndex.tsx | 2 +- .../src/tables/InvenTreeTableHeader.tsx | 19 +- src/frontend/src/tables/Search.tsx | 3 + .../tables/purchasing/SupplierPartTable.tsx | 2 +- src/frontend/tests/helpers.ts | 1 + src/frontend/tests/pages/pui_company.spec.ts | 40 ++++ src/frontend/tests/pui_general.spec.ts | 75 ++++--- 12 files changed, 290 insertions(+), 121 deletions(-) create mode 100644 src/frontend/tests/pages/pui_company.spec.ts diff --git a/src/backend/InvenTree/InvenTree/api.py b/src/backend/InvenTree/InvenTree/api.py index f4d528e5ad..6b9b9c7056 100644 --- a/src/backend/InvenTree/InvenTree/api.py +++ b/src/backend/InvenTree/InvenTree/api.py @@ -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' diff --git a/src/frontend/src/components/nav/SearchDrawer.tsx b/src/frontend/src/components/nav/SearchDrawer.tsx index 60d4b33efb..dc0e091b00 100644 --- a/src/frontend/src/components/nav/SearchDrawer.tsx +++ b/src/frontend/src/components/nav/SearchDrawer.tsx @@ -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,50 +60,102 @@ 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: + }); + } + }, + [overviewUrl, searchText] + ); + if (query.results.count == 0) { return null; } - const model = getModelInfo(query.model); - return ( - - - + + + - {model.label_multiple} + + + + + + {query.title ?? modelInfo.label_multiple} {' '} - {query.results.count} results - - onRemove(query.model)} - > - - + + + onRemove(query.model)} + > + + + + + - + + {query.results.results.map((result: any) => ( ))} - - - + + ); } @@ -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({ - + @@ -443,16 +525,24 @@ export function SearchDrawer({ )} {!searchQuery.isFetching && !searchQuery.isError && ( - {queryResults.map((query, idx) => ( - removeResults(query)} - onResultClick={(query, pk, event) => - onResultClick(query, pk, event) - } - /> - ))} + q.model)} + > + {queryResults.map((query, idx) => ( + removeResults(query)} + onResultClick={(query, pk, event) => + onResultClick(query, pk, event) + } + /> + ))} + )} {searchQuery.isError && ( diff --git a/src/frontend/src/components/render/ModelType.tsx b/src/frontend/src/components/render/ModelType.tsx index cea94f46bf..c65ccb8ffa 100644 --- a/src/frontend/src/components/render/ModelType.tsx +++ b/src/frontend/src/components/render/ModelType.tsx @@ -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' }, diff --git a/src/frontend/src/pages/company/CompanyDetail.tsx b/src/frontend/src/pages/company/CompanyDetail.tsx index 8b7a63e983..8776409442 100644 --- a/src/frontend/src/pages/company/CompanyDetail.tsx +++ b/src/frontend/src/pages/company/CompanyDetail.tsx @@ -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) { icon: , content: detailsPanel }, - { - name: 'manufactured-parts', - label: t`Manufactured Parts`, - icon: , - hidden: !company?.is_manufacturer, - content: company?.pk && ( - - ) - }, { name: 'supplied-parts', label: t`Supplied Parts`, - icon: , + icon: , hidden: !company?.is_supplier, content: company?.pk && ( ) }, + { + name: 'manufactured-parts', + label: t`Manufactured Parts`, + icon: , + hidden: !company?.is_manufacturer, + content: company?.pk && ( + + ) + }, { name: 'purchase-orders', label: t`Purchase Orders`, diff --git a/src/frontend/src/pages/purchasing/PurchasingIndex.tsx b/src/frontend/src/pages/purchasing/PurchasingIndex.tsx index 564ca91e40..b540664a3c 100644 --- a/src/frontend/src/pages/purchasing/PurchasingIndex.tsx +++ b/src/frontend/src/pages/purchasing/PurchasingIndex.tsx @@ -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: , + content: + }, { 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: , + content: } ]; }, [user]); diff --git a/src/frontend/src/pages/sales/SalesIndex.tsx b/src/frontend/src/pages/sales/SalesIndex.tsx index 1ac981183f..3b0f26815b 100644 --- a/src/frontend/src/pages/sales/SalesIndex.tsx +++ b/src/frontend/src/pages/sales/SalesIndex.tsx @@ -36,7 +36,7 @@ export default function PurchasingIndex() { hidden: !user.hasViewRole(UserRoles.return_order) }, { - name: 'suppliers', + name: 'customers', label: t`Customers`, icon: , content: ( diff --git a/src/frontend/src/tables/InvenTreeTableHeader.tsx b/src/frontend/src/tables/InvenTreeTableHeader.tsx index f8241da173..bfc1d15894 100644 --- a/src/frontend/src/tables/InvenTreeTableHeader.tsx +++ b/src/frontend/src/tables/InvenTreeTableHeader.tsx @@ -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({ /> )} - {tableState.queryFilters.size > 0 && ( + {(hasCustomFilters || hasCustomSearch) && ( tableState.clearQueryFilters()} /> )} - {tableProps.enableSearch && ( tableState.setSearchTerm(term)} /> )} @@ -208,6 +220,7 @@ export default function InvenTreeTableHeader({ disabled={tableState.activeFilters?.length == 0} > diff --git a/src/frontend/src/tables/Search.tsx b/src/frontend/src/tables/Search.tsx index 84bb0f090b..1d847da409 100644 --- a/src/frontend/src/tables/Search.tsx +++ b/src/frontend/src/tables/Search.tsx @@ -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(''); @@ -19,6 +21,7 @@ export function TableSearchInput({ return ( } placeholder={t`Search`} onChange={(event) => setValue(event.target.value)} diff --git a/src/frontend/src/tables/purchasing/SupplierPartTable.tsx b/src/frontend/src/tables/purchasing/SupplierPartTable.tsx index 5d4c65c022..5f836ce5f4 100644 --- a/src/frontend/src/tables/purchasing/SupplierPartTable.tsx +++ b/src/frontend/src/tables/purchasing/SupplierPartTable.tsx @@ -73,7 +73,7 @@ export function SupplierPartTable({ DescriptionColumn({}), { accessor: 'manufacturer', - + title: t`Manufacturer`, sortable: true, render: (record: any) => { const manufacturer = record?.manufacturer_detail ?? {}; diff --git a/src/frontend/tests/helpers.ts b/src/frontend/tests/helpers.ts index cb4e03096a..a77121442c 100644 --- a/src/frontend/tests/helpers.ts +++ b/src/frontend/tests/helpers.ts @@ -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'); }; diff --git a/src/frontend/tests/pages/pui_company.spec.ts b/src/frontend/tests/pages/pui_company.spec.ts new file mode 100644 index 0000000000..548ab81819 --- /dev/null +++ b/src/frontend/tests/pages/pui_company.spec.ts @@ -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(); +}); diff --git a/src/frontend/tests/pui_general.spec.ts b/src/frontend/tests/pui_general.spec.ts index b9340208b7..d56bd937be 100644 --- a/src/frontend/tests/pui_general.spec.ts +++ b/src/frontend/tests/pui_general.spec.ts @@ -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(); +});