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({
-