Repository: saleor/saleor
Pull request: Filtering: use search in a consistent way
I want to merge this change because:
- [x] resolve #4007 
- [x] resolve #4711 

<!-- Please mention all relevant issue numbers. -->

### Screenshots

<!-- If your changes affect the UI, providing "before" and "after" screenshots will
greatly reduce the amount of work needed to review your work. -->

### Pull Request Checklist

<!-- Please keep this section. It will make maintainer's life easier. -->

1. [ ] Privileged views and APIs are guarded by proper permission checks.
1. [ ] All visible strings are translated with proper context.
1. [ ] All data-formatting is locale-aware (dates, numbers, and so on).
1. [ ] Database queries are optimized and the number of queries is constant.
1. [ ] Database migration files are up to date.
1. [ ] The changes are tested.
1. [ ] GraphQL schema and type definitions are up to date.
1. [ ] Changes are mentioned in the changelog.


Problem statement:
Filtering: use search in a consistent way
Some filters have the `search` filter which doesn't work in a consistent manner - for some filters it does `icontains` filtering (`filter_by_query_param` function) while products resolver uses database search (see the `ProductFilter` class). We should unify this and use search for all `search` filters.



Extend the ProductOrderField
### What I'm trying to achieve
To sort by product type name and publication status. 

### Describe a proposed solution
Add these fields to `ProductOrderField`.

Hints from issue discussion:
Another problem is that we have two ways to search for things in the API - either by using `query` field (top-level argument in many resolvers) or `search` inside filter objects.
We'll make sure that all connection fields that we want to have search capabilities will have the `filter` object with the `search` field inside. Currently, we're missing it in the following fields:

- `categories`
- `pages`
- `orders`
- `draftOrders`
- `menus`
- `pluginConfiguration`

After we add `search` consistently, `query` field will need to be deprecated.

Commit message: Add changelog and graphql schema

Patch to review:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fe84fe6e051..9101908f542 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -65,6 +65,7 @@ All notable, unreleased changes to this project will be documented in this file.
 - Distinguish OrderLine product name and variant name - #4702 by @fowczarek
 - Fix for Digital products - update order status after automatic fulfillment - #4709 by @korycins
 - Add redirectUrl to staff and user create mutations - #4717 by @fowczarek
+- Filtering: use search in a consistent way. Add sort by product type name and publication status to products query. - #4715 by @fowczarek
 
 ## 2.8.0
 
diff --git a/saleor/extensions/manager.py b/saleor/extensions/manager.py
index 63d033d276b..b6cadb21656 100644
--- a/saleor/extensions/manager.py
+++ b/saleor/extensions/manager.py
@@ -218,14 +218,14 @@ def get_plugin_configuration(self, plugin_name) -> "PluginConfiguration":
                 return plugin.get_plugin_configuration(plugin_configurations_qs)
 
     def get_plugin_configurations(self) -> List["PluginConfiguration"]:
-        plugin_configurations = []
+        plugin_configuration_ids = []
         plugin_configurations_qs = PluginConfiguration.objects.all()
         for plugin in self.plugins:
             plugin_configuration = plugin.get_plugin_configuration(
                 plugin_configurations_qs
             )
-            plugin_configurations.append(plugin_configuration)
-        return plugin_configurations
+            plugin_configuration_ids.append(plugin_configuration.pk)
+        return PluginConfiguration.objects.filter(pk__in=plugin_configuration_ids)
 
 
 def get_extensions_manager(
diff --git a/saleor/graphql/extensions/filters.py b/saleor/graphql/extensions/filters.py
new file mode 100644
index 00000000000..66df79f39de
--- /dev/null
+++ b/saleor/graphql/extensions/filters.py
@@ -0,0 +1,25 @@
+import django_filters
+
+from ...extensions.models import PluginConfiguration
+from ..core.types import FilterInputObjectType
+from ..utils import filter_by_query_param
+
+
+def filter_plugin_search(qs, _, value):
+    plugin_fields = ["name", "description"]
+    qs = filter_by_query_param(qs, value, plugin_fields)
+    return qs
+
+
+class PluginFilter(django_filters.FilterSet):
+    active = django_filters.BooleanFilter()
+    search = django_filters.CharFilter(method=filter_plugin_search)
+
+    class Meta:
+        model = PluginConfiguration
+        fields = ["active", "search"]
+
+
+class PluginFilterInput(FilterInputObjectType):
+    class Meta:
+        filterset_class = PluginFilter
diff --git a/saleor/graphql/extensions/schema.py b/saleor/graphql/extensions/schema.py
index 125c5f74fbb..78587f26034 100644
--- a/saleor/graphql/extensions/schema.py
+++ b/saleor/graphql/extensions/schema.py
@@ -1,6 +1,7 @@
 import graphene
 
-from ..core.fields import PrefetchingConnectionField
+from ..core.fields import FilterInputConnectionField
+from .filters import PluginFilterInput
 from ..decorators import permission_required
 from .mutations import PluginUpdate
 from .resolvers import resolve_plugin, resolve_plugins
@@ -13,7 +14,9 @@ class ExtensionsQueries(graphene.ObjectType):
         id=graphene.Argument(graphene.ID, required=True),
         description="Lookup a plugin by ID.",
     )
-    plugins = PrefetchingConnectionField(Plugin, description="List of plugins")
+    plugins = FilterInputConnectionField(
+        Plugin, filter=PluginFilterInput(), description="List of plugins"
+    )
 
     @permission_required("extensions.manage_plugins")
     def resolve_plugin(self, info, **data):
diff --git a/saleor/graphql/menu/filters.py b/saleor/graphql/menu/filters.py
new file mode 100644
index 00000000000..e4470c68a9d
--- /dev/null
+++ b/saleor/graphql/menu/filters.py
@@ -0,0 +1,43 @@
+import django_filters
+
+from ...menu.models import Menu, MenuItem
+from ..core.types import FilterInputObjectType
+from ..utils import filter_by_query_param
+
+
+def filter_menu_search(qs, _, value):
+    menu_fields = ["name"]
+    qs = filter_by_query_param(qs, value, menu_fields)
+    return qs
+
+
+def filter_menu_item_search(qs, _, value):
+    menu_item_fields = ["name"]
+    qs = filter_by_query_param(qs, value, menu_item_fields)
+    return qs
+
+
+class MenuFilter(django_filters.FilterSet):
+    search = django_filters.CharFilter(method=filter_menu_search)
+
+    class Meta:
+        model = Menu
+        fields = ["search"]
+
+
+class MenuItemFilter(django_filters.FilterSet):
+    search = django_filters.CharFilter(method=filter_menu_item_search)
+
+    class Meta:
+        model = MenuItem
+        fields = ["search"]
+
+
+class MenuFilterInput(FilterInputObjectType):
+    class Meta:
+        filterset_class = MenuFilter
+
+
+class MenuItemFilterInput(FilterInputObjectType):
+    class Meta:
+        filterset_class = MenuItemFilter
diff --git a/saleor/graphql/menu/schema.py b/saleor/graphql/menu/schema.py
index 6819b9e2cad..095630d4332 100644
--- a/saleor/graphql/menu/schema.py
+++ b/saleor/graphql/menu/schema.py
@@ -1,9 +1,10 @@
 import graphene
 
-from ..core.fields import PrefetchingConnectionField
+from ..core.fields import FilterInputConnectionField
 from ..descriptions import DESCRIPTIONS
 from ..translations.mutations import MenuItemTranslate
 from .bulk_mutations import MenuBulkDelete, MenuItemBulkDelete
+from .filters import MenuFilterInput, MenuItemFilterInput
 from .mutations import (
     AssignNavigation,
     MenuCreate,
@@ -25,9 +26,10 @@ class MenuQueries(graphene.ObjectType):
         name=graphene.Argument(graphene.String, description="Menu name."),
         description="Lookup a menu by ID or name.",
     )
-    menus = PrefetchingConnectionField(
+    menus = FilterInputConnectionField(
         Menu,
         query=graphene.String(description=DESCRIPTIONS["menu"]),
+        filter=MenuFilterInput(),
         description="List of the shop's menus.",
     )
     menu_item = graphene.Field(
@@ -35,9 +37,10 @@ class MenuQueries(graphene.ObjectType):
         id=graphene.Argument(graphene.ID, required=True),
         description="Lookup a menu item by ID.",
     )
-    menu_items = PrefetchingConnectionField(
+    menu_items = FilterInputConnectionField(
         MenuItem,
         query=graphene.String(description=DESCRIPTIONS["menu_item"]),
+        filter=MenuItemFilterInput(),
         description="List of the shop's menu items.",
     )
 
diff --git a/saleor/graphql/order/filters.py b/saleor/graphql/order/filters.py
index 67898a4f0f9..e64448bdfde 100644
--- a/saleor/graphql/order/filters.py
+++ b/saleor/graphql/order/filters.py
@@ -55,13 +55,27 @@ def filter_created_range(qs, _, value):
     return qs
 
 
+def filter_order_search(qs, _, value):
+    order_fields = [
+        "discount_name",
+        "token",
+        "user_email",
+        "user__email",
+        "user__first_name",
+        "user__last_name",
+    ]
+    qs = filter_by_query_param(qs, value, order_fields)
+    return qs
+
+
 class DraftOrderFilter(django_filters.FilterSet):
     customer = django_filters.CharFilter(method=filter_customer)
     created = ObjectTypeFilter(input_class=DateRangeInput, method=filter_created_range)
+    search = django_filters.CharFilter(method=filter_order_search)
 
     class Meta:
         model = Order
-        fields = ["customer", "created"]
+        fields = ["customer", "created", "search"]
 
 
 class OrderFilter(DraftOrderFilter):
@@ -71,7 +85,8 @@ class OrderFilter(DraftOrderFilter):
     status = ListObjectTypeFilter(input_class=OrderStatusFilter, method=filter_status)
     customer = django_filters.CharFilter(method=filter_customer)
     created = ObjectTypeFilter(input_class=DateRangeInput, method=filter_created_range)
+    search = django_filters.CharFilter(method=filter_order_search)
 
     class Meta:
         model = Order
-        fields = ["payment_status", "status", "customer", "created"]
+        fields = ["payment_status", "status", "customer", "created", "search"]
diff --git a/saleor/graphql/page/filters.py b/saleor/graphql/page/filters.py
new file mode 100644
index 00000000000..45473db131b
--- /dev/null
+++ b/saleor/graphql/page/filters.py
@@ -0,0 +1,24 @@
+import django_filters
+
+from ...page.models import Page
+from ..core.types import FilterInputObjectType
+from ..utils import filter_by_query_param
+
+
+def filter_page_search(qs, _, value):
+    page_fields = ["content", "slug", "title"]
+    qs = filter_by_query_param(qs, value, page_fields)
+    return qs
+
+
+class PageFilter(django_filters.FilterSet):
+    search = django_filters.CharFilter(method=filter_page_search)
+
+    class Meta:
+        model = Page
+        fields = ["search"]
+
+
+class PageFilterInput(FilterInputObjectType):
+    class Meta:
+        filterset_class = PageFilter
diff --git a/saleor/graphql/page/schema.py b/saleor/graphql/page/schema.py
index 1f8979101f5..9d234e7ac4b 100644
--- a/saleor/graphql/page/schema.py
+++ b/saleor/graphql/page/schema.py
@@ -1,9 +1,10 @@
 import graphene
 
-from ..core.fields import PrefetchingConnectionField
+from ..core.fields import FilterInputConnectionField
 from ..descriptions import DESCRIPTIONS
 from ..translations.mutations import PageTranslate
 from .bulk_mutations import PageBulkDelete, PageBulkPublish
+from .filters import PageFilterInput
 from .mutations import PageCreate, PageDelete, PageUpdate
 from .resolvers import resolve_page, resolve_pages
 from .types import Page
@@ -16,9 +17,10 @@ class PageQueries(graphene.ObjectType):
         slug=graphene.String(),
         description="Lookup a page by ID or by slug.",
     )
-    pages = PrefetchingConnectionField(
+    pages = FilterInputConnectionField(
         Page,
         query=graphene.String(description=DESCRIPTIONS["page"]),
+        filter=PageFilterInput(),
         description="List of the shop's pages.",
     )
 
diff --git a/saleor/graphql/product/enums.py b/saleor/graphql/product/enums.py
index 571b8e91062..6e7152a40e8 100644
--- a/saleor/graphql/product/enums.py
+++ b/saleor/graphql/product/enums.py
@@ -28,6 +28,8 @@ class ProductOrderField(graphene.Enum):
     PRICE = "price_amount"
     MINIMAL_PRICE = "minimal_variant_price_amount"
     DATE = "updated_at"
+    TYPE = "product_type__name"
+    PUBLISHED = "is_published"
 
     @property
     def description(self):
@@ -39,6 +41,10 @@ def description(self):
             return "Sort products by a minimal price of a product's variant."
         if self == ProductOrderField.DATE:
             return "Sort products by update date."
+        if self == ProductOrderField.TYPE:
+            return "Sort products by type."
+        if self == ProductOrderField.PUBLISHED:
+            return "Sort products by publication status."
         raise ValueError("Unsupported enum value: %s" % self.value)
 
 
diff --git a/saleor/graphql/product/filters.py b/saleor/graphql/product/filters.py
index 9e3a9b2278a..355c7ef7e98 100644
--- a/saleor/graphql/product/filters.py
+++ b/saleor/graphql/product/filters.py
@@ -6,7 +6,7 @@
 from django.db.models import Q, Sum
 from graphene_django.filter import GlobalIDFilter, GlobalIDMultipleChoiceFilter
 
-from ...product.models import Attribute, Collection, Product, ProductType
+from ...product.models import Attribute, Category, Collection, Product, ProductType
 from ...search.backends import picker
 from ..core.filters import EnumFilter, ListObjectTypeFilter, ObjectTypeFilter
 from ..core.types import FilterInputObjectType
@@ -231,6 +231,16 @@ class Meta:
         fields = ["published", "search"]
 
 
+class CategoryFilter(django_filters.FilterSet):
+    search = django_filters.CharFilter(
+        method=filter_fields_containing_value("slug", "name", "description")
+    )
+
+    class Meta:
+        model = Category
+        fields = ["search"]
+
+
 class ProductTypeFilter(django_filters.FilterSet):
     search = django_filters.CharFilter(method=filter_fields_containing_value("name"))
 
@@ -274,6 +284,11 @@ class Meta:
         filterset_class = CollectionFilter
 
 
+class CategoryFilterInput(FilterInputObjectType):
+    class Meta:
+        filterset_class = CategoryFilter
+
+
 class ProductTypeFilterInput(FilterInputObjectType):
     class Meta:
         filterset_class = ProductTypeFilter
diff --git a/saleor/graphql/product/schema.py b/saleor/graphql/product/schema.py
index 7b5764142fe..a1831c11298 100644
--- a/saleor/graphql/product/schema.py
+++ b/saleor/graphql/product/schema.py
@@ -26,6 +26,7 @@
 from .enums import StockAvailability
 from .filters import (
     AttributeFilterInput,
+    CategoryFilterInput,
     CollectionFilterInput,
     ProductFilterInput,
     ProductTypeFilterInput,
@@ -156,9 +157,10 @@ class ProductQueries(graphene.ObjectType):
         id=graphene.Argument(graphene.ID, required=True),
         description="Lookup an attribute by ID.",
     )
-    categories = PrefetchingConnectionField(
+    categories = FilterInputConnectionField(
         Category,
         query=graphene.String(description=DESCRIPTIONS["category"]),
+        filter=CategoryFilterInput(),
         level=graphene.Argument(graphene.Int),
         description="List of the shop's categories.",
     )
diff --git a/saleor/graphql/schema.graphql b/saleor/graphql/schema.graphql
index c85b0d30a2a..2329832e6bd 100644
--- a/saleor/graphql/schema.graphql
+++ b/saleor/graphql/schema.graphql
@@ -466,6 +466,10 @@ type CategoryDelete {
   category: Category
 }
 
+input CategoryFilterInput {
+  search: String
+}
+
 input CategoryInput {
   description: String
   descriptionJson: JSONString
@@ -1627,6 +1631,10 @@ type MenuDelete {
   menu: Menu
 }
 
+input MenuFilterInput {
+  search: String
+}
+
 input MenuInput {
   name: String
 }
@@ -1682,6 +1690,10 @@ type MenuItemDelete {
   menuItem: MenuItem
 }
 
+input MenuItemFilterInput {
+  search: String
+}
+
 input MenuItemInput {
   name: String
   url: String
@@ -2100,6 +2112,7 @@ enum OrderDirection {
 input OrderDraftFilterInput {
   customer: String
   created: DateRangeInput
+  search: String
 }
 
 type OrderEvent implements Node {
@@ -2176,6 +2189,7 @@ input OrderFilterInput {
   status: [OrderStatusFilter]
   customer: String
   created: DateRangeInput
+  search: String
 }
 
 type OrderLine implements Node {
@@ -2301,6 +2315,10 @@ type PageDelete {
   page: Page
 }
 
+input PageFilterInput {
+  search: String
+}
+
 type PageInfo {
   hasNextPage: Boolean!
   hasPreviousPage: Boolean!
@@ -2470,6 +2488,11 @@ type PluginCountableEdge {
   cursor: String!
 }
 
+input PluginFilterInput {
+  active: Boolean
+  search: String
+}
+
 type PluginUpdate {
   errors: [Error!]
   plugin: Plugin
@@ -2669,6 +2692,8 @@ enum ProductOrderField {
   PRICE
   MINIMAL_PRICE
   DATE
+  TYPE
+  PUBLISHED
 }
 
 type ProductPricingInfo {
@@ -2930,7 +2955,7 @@ type Query {
   digitalContents(query: String, level: Int, before: String, after: String, first: Int, last: Int): DigitalContentCountableConnection
   attributes(query: String, inCategory: ID, inCollection: ID, filter: AttributeFilterInput, sortBy: AttributeSortingInput, before: String, after: String, first: Int, last: Int): AttributeCountableConnection
   attribute(id: ID!): Attribute
-  categories(query: String, level: Int, before: String, after: String, first: Int, last: Int): CategoryCountableConnection
+  categories(query: String, filter: CategoryFilterInput, level: Int, before: String, after: String, first: Int, last: Int): CategoryCountableConnection
   category(id: ID!): Category
   collection(id: ID!): Collection
   collections(filter: CollectionFilterInput, query: String, before: String, after: String, first: Int, last: Int): CollectionCountableConnection
@@ -2945,7 +2970,7 @@ type Query {
   payments(before: String, after: String, first: Int, last: Int): PaymentCountableConnection
   paymentClientToken(gateway: GatewaysEnum): String
   page(id: ID, slug: String): Page
-  pages(query: String, before: String, after: String, first: Int, last: Int): PageCountableConnection
+  pages(query: String, filter: PageFilterInput, before: String, after: String, first: Int, last: Int): PageCountableConnection
   homepageEvents(before: String, after: String, first: Int, last: Int): OrderEventCountableConnection
   order(id: ID!): Order
   orders(filter: OrderFilterInput, query: String, created: ReportingPeriod, status: OrderStatusFilter, before: String, after: String, first: Int, last: Int): OrderCountableConnection
@@ -2953,13 +2978,13 @@ type Query {
   ordersTotal(period: ReportingPeriod): TaxedMoney
   orderByToken(token: String!): Order
   menu(id: ID, name: String): Menu
-  menus(query: String, before: String, after: String, first: Int, last: Int): MenuCountableConnection
+  menus(query: String, filter: MenuFilterInput, before: String, after: String, first: Int, last: Int): MenuCountableConnection
   menuItem(id: ID!): MenuItem
-  menuItems(query: String, before: String, after: String, first: Int, last: Int): MenuItemCountableConnection
+  menuItems(query: String, filter: MenuItemFilterInput, before: String, after: String, first: Int, last: Int): MenuItemCountableConnection
   giftCard(id: ID!): GiftCard
   giftCards(before: String, after: String, first: Int, last: Int): GiftCardCountableConnection
   plugin(id: ID!): Plugin
-  plugins(before: String, after: String, first: Int, last: Int): PluginCountableConnection
+  plugins(filter: PluginFilterInput, before: String, after: String, first: Int, last: Int): PluginCountableConnection
   sale(id: ID!): Sale
   sales(filter: SaleFilterInput, query: String, before: String, after: String, first: Int, last: Int): SaleCountableConnection
   voucher(id: ID!): Voucher
diff --git a/saleor/graphql/utils.py b/saleor/graphql/utils.py
index bb9b9bbec20..eebe9f0c77d 100644
--- a/saleor/graphql/utils.py
+++ b/saleor/graphql/utils.py
@@ -139,11 +139,15 @@ def filter_by_period(queryset, period, field_name):
 
 
 def generate_query_argument_description(search_fields):
+    deprecated_info = (
+        "DEPRECATED: Will be removed in Saleor 2.10,"
+        ' use "filter: {search: {}}" instead.\n'
+    )
     header = "Supported filter parameters:\n"
     supported_list = ""
     for field in search_fields:
         supported_list += "* {0}\n".format(field)
-    return header + supported_list
+    return deprecated_info + header + supported_list
 
 
 def format_permissions_for_display(permissions):
diff --git a/tests/api/test_extensions.py b/tests/api/test_extensions.py
index f97fa8d112f..3c3fc5d50b1 100644
--- a/tests/api/test_extensions.py
+++ b/tests/api/test_extensions.py
@@ -220,3 +220,91 @@ def test_plugin_configuration_update(
     assert second_configuration_item["type"] == old_configuration[1]["type"]
     assert second_configuration_item["help_text"] == old_configuration[1]["help_text"]
     assert second_configuration_item["label"] == old_configuration[1]["label"]
+
+
+class Plugin1(BasePlugin):
+    PLUGIN_NAME = "Plugin1"
+
+    @classmethod
+    def get_plugin_configuration(cls, queryset) -> "PluginConfiguration":
+        qs = queryset.filter(name="Plugin1")
+        if qs.exists():
+            return qs[0]
+        defaults = {
+            "name": "Plugin1",
+            "description": "Test plugin description_1",
+            "active": True,
+            "configuration": [],
+        }
+        return PluginConfiguration.objects.create(**defaults)
+
+
+class Plugin2Inactive(BasePlugin):
+    PLUGIN_NAME = "Plugin2Inactive"
+
+    @classmethod
+    def get_plugin_configuration(cls, queryset) -> "PluginConfiguration":
+        qs = queryset.filter(name="Plugin2Inactive")
+        if qs.exists():
+            return qs[0]
+        defaults = {
+            "name": "Plugin2Inactive",
+            "description": "Test plugin description_2",
+            "active": False,
+            "configuration": [],
+        }
+        return PluginConfiguration.objects.create(**defaults)
+
+
+class Active(BasePlugin):
+    PLUGIN_NAME = "Plugin1"
+
+    @classmethod
+    def get_plugin_configuration(cls, queryset) -> "PluginConfiguration":
+        qs = queryset.filter(name="Active")
+        if qs.exists():
+            return qs[0]
+        defaults = {
+            "name": "Active",
+            "description": "Not working",
+            "active": True,
+            "configuration": [],
+        }
+        return PluginConfiguration.objects.create(**defaults)
+
+
+@pytest.mark.parametrize(
+    "plugin_filter, count",
+    [
+        ({"search": "Plugin1"}, 1),
+        ({"search": "description"}, 2),
+        ({"active": True}, 2),
+        ({"search": "Plugin"}, 2),
+        ({"active": "False", "search": "Plugin"}, 1),
+    ],
+)
+def test_plugins_query_with_filter(
+    plugin_filter, count, staff_api_client, permission_manage_plugins, settings
+):
+    settings.PLUGINS = [
+        "tests.api.test_extensions.Plugin1",
+        "tests.api.test_extensions.Plugin2Inactive",
+        "tests.api.test_extensions.Active",
+    ]
+    query = """
+        query ($filter: PluginFilterInput) {
+            plugins(first: 5, filter:$filter) {
+                totalCount
+                edges {
+                    node {
+                        id
+                    }
+                }
+            }
+        }
+    """
+    variables = {"filter": plugin_filter}
+    staff_api_client.user.user_permissions.add(permission_manage_plugins)
+    response = staff_api_client.post_graphql(query, variables)
+    content = get_graphql_content(response)
+    assert content["data"]["plugins"]["totalCount"] == count
diff --git a/tests/api/test_graphql.py b/tests/api/test_graphql.py
index 88a891a9685..6f33437cdd5 100644
--- a/tests/api/test_graphql.py
+++ b/tests/api/test_graphql.py
@@ -267,6 +267,10 @@ def test_filter_by_query_param(qs):
 
 
 def test_generate_query_argument_description():
-    expected = "Supported filter parameters:\n* field_1\n* field_2\n"
+    expected = (
+        "DEPRECATED: Will be removed in Saleor 2.10,"
+        ' use "filter: {search: {}}" instead.'
+        "\nSupported filter parameters:\n* field_1\n* field_2\n"
+    )
     field_list = ["field_1", "field_2"]
     assert generate_query_argument_description(field_list) == expected
diff --git a/tests/api/test_menu.py b/tests/api/test_menu.py
index c870c9bda6b..8d6ce9f4cac 100644
--- a/tests/api/test_menu.py
+++ b/tests/api/test_menu.py
@@ -90,6 +90,33 @@ def test_menus_query(user_api_client, menu, menu_item):
     assert item["menu"]["name"] == menu.name
 
 
+@pytest.mark.parametrize(
+    "menu_filter, count", [({"search": "Menu1"}, 1), ({"search": "Menu"}, 2)]
+)
+def test_menus_query_with_filter(
+    menu_filter, count, staff_api_client, permission_manage_menus
+):
+    query = """
+        query ($filter: MenuFilterInput) {
+            menus(first: 5, filter:$filter) {
+                totalCount
+                edges {
+                    node {
+                        id
+                    }
+                }
+            }
+        }
+    """
+    Menu.objects.create(name="Menu1")
+    Menu.objects.create(name="Menu2")
+    variables = {"filter": menu_filter}
+    staff_api_client.user.user_permissions.add(permission_manage_menus)
+    response = staff_api_client.post_graphql(query, variables)
+    content = get_graphql_content(response)
+    assert content["data"]["menus"]["totalCount"] == count
+
+
 def test_menu_items_query(user_api_client, menu_item, collection):
     query = """
     query menuitem($id: ID!) {
@@ -130,6 +157,36 @@ def test_menu_items_query(user_api_client, menu_item, collection):
     assert data["url"] is None
 
 
+@pytest.mark.parametrize(
+    "menu_item_filter, count",
+    [({"search": "MenuItem1"}, 1), ({"search": "MenuItem"}, 2)],
+)
+def test_menu_items_query_with_filter(
+    menu_item_filter, count, staff_api_client, permission_manage_menus
+):
+    query = """
+        query ($filter: MenuItemFilterInput) {
+            menuItems(first: 5, filter:$filter) {
+                totalCount
+                edges {
+                    node {
+                        id
+                        name
+                    }
+                }
+            }
+        }
+    """
+    menu = Menu.objects.create(name="Menu1")
+    MenuItem.objects.create(name="MenuItem1", menu=menu)
+    MenuItem.objects.create(name="MenuItem2", menu=menu)
+    variables = {"filter": menu_item_filter}
+    staff_api_client.user.user_permissions.add(permission_manage_menus)
+    response = staff_api_client.post_graphql(query, variables)
+    content = get_graphql_content(response)
+    assert content["data"]["menuItems"]["totalCount"] == count
+
+
 def test_menu_item_query_static_url(user_api_client, menu_item):
     query = """
     query menuitem($id: ID!) {
diff --git a/tests/api/test_order.py b/tests/api/test_order.py
index c0045a61873..c8c2215818d 100644
--- a/tests/api/test_order.py
+++ b/tests/api/test_order.py
@@ -33,6 +33,7 @@ def orders_query_with_filter():
     query = """
       query ($filter: OrderFilterInput!, ) {
         orders(first: 5, filter:$filter) {
+          totalCount
           edges {
             node {
               id
@@ -49,6 +50,7 @@ def draft_orders_query_with_filter():
     query = """
       query ($filter: OrderDraftFilterInput!, ) {
         draftOrders(first: 5, filter:$filter) {
+          totalCount
           edges {
             node {
               id
@@ -2110,3 +2112,88 @@ def test_order_query_with_filter_created_(
     orders = content["data"]["draftOrders"]["edges"]
 
     assert len(orders) == count
+
+
+@pytest.mark.parametrize(
+    "orders_filter, count",
+    [
+        ({"search": "test_token"}, 1),
+        ({"search": "test_discount"}, 1),
+        ({"search": "user"}, 2),
+        ({"search": "user1@example.com"}, 1),
+        ({"search": "test@example.com"}, 1),
+        ({"search": "Leslie"}, 1),
+        ({"search": "Wade"}, 1),
+        ({"search": ""}, 3),
+    ],
+)
+def test_orders_query_with_filter_search(
+    orders_filter,
+    count,
+    orders_query_with_filter,
+    staff_api_client,
+    permission_manage_orders,
+    customer_user,
+):
+    Order.objects.bulk_create(
+        [
+            Order(
+                user=customer_user, token="test_token", discount_name="test_discount"
+            ),
+            Order(token=str(uuid.uuid4()), user_email="user1@example.com"),
+            Order(token=str(uuid.uuid4()), user_email="user2@example.com"),
+        ]
+    )
+    variables = {"filter": orders_filter}
+    staff_api_client.user.user_permissions.add(permission_manage_orders)
+    response = staff_api_client.post_graphql(orders_query_with_filter, variables)
+    content = get_graphql_content(response)
+    assert content["data"]["orders"]["totalCount"] == count
+
+
+@pytest.mark.parametrize(
+    "draft_orders_filter, count",
+    [
+        ({"search": "test_token"}, 1),
+        ({"search": "test_discount"}, 1),
+        ({"search": "user"}, 2),
+        ({"search": "user1@example.com"}, 1),
+        ({"search": "test@example.com"}, 1),
+        ({"search": "Leslie"}, 1),
+        ({"search": "Wade"}, 1),
+        ({"search": ""}, 3),
+    ],
+)
+def test_draft_orders_query_with_filter_search(
+    draft_orders_filter,
+    count,
+    draft_orders_query_with_filter,
+    staff_api_client,
+    permission_manage_orders,
+    customer_user,
+):
+    Order.objects.bulk_create(
+        [
+            Order(
+                user=customer_user,
+                token="test_token",
+                discount_name="test_discount",
+                status=OrderStatus.DRAFT,
+            ),
+            Order(
+                token=str(uuid.uuid4()),
+                user_email="user1@example.com",
+                status=OrderStatus.DRAFT,
+            ),
+            Order(
+                token=str(uuid.uuid4()),
+                user_email="user2@example.com",
+                status=OrderStatus.DRAFT,
+            ),
+        ]
+    )
+    variables = {"filter": draft_orders_filter}
+    staff_api_client.user.user_permissions.add(permission_manage_orders)
+    response = staff_api_client.post_graphql(draft_orders_query_with_filter, variables)
+    content = get_graphql_content(response)
+    assert content["data"]["draftOrders"]["totalCount"] == count
diff --git a/tests/api/test_page.py b/tests/api/test_page.py
index ae5c32283e2..93336962331 100644
--- a/tests/api/test_page.py
+++ b/tests/api/test_page.py
@@ -259,3 +259,38 @@ def test_bulk_unpublish(staff_api_client, page_list, permission_manage_pages):
 
     assert content["data"]["pageBulkPublish"]["count"] == len(page_list)
     assert not any(page.is_published for page in page_list)
+
+
+@pytest.mark.parametrize(
+    "page_filter, count",
+    [
+        ({"search": "Page1"}, 1),
+        ({"search": "slug_page_2"}, 1),
+        ({"search": "test"}, 1),
+        ({"search": "slug_"}, 3),
+        ({"search": "Page"}, 2),
+    ],
+)
+def test_pages_query_with_filter(
+    page_filter, count, staff_api_client, permission_manage_pages
+):
+    query = """
+        query ($filter: PageFilterInput) {
+            pages(first: 5, filter:$filter) {
+                totalCount
+                edges {
+                    node {
+                        id
+                    }
+                }
+            }
+        }
+    """
+    Page.objects.create(title="Page1", slug="slug_page_1", content="Content for page 1")
+    Page.objects.create(title="Page2", slug="slug_page_2", content="Content for page 2")
+    Page.objects.create(title="About", slug="slug_about", content="About test content")
+    variables = {"filter": page_filter}
+    staff_api_client.user.user_permissions.add(permission_manage_pages)
+    response = staff_api_client.post_graphql(query, variables)
+    content = get_graphql_content(response)
+    assert content["data"]["pages"]["totalCount"] == count
diff --git a/tests/api/test_product.py b/tests/api/test_product.py
index 2ae09d843fd..69906977236 100644
--- a/tests/api/test_product.py
+++ b/tests/api/test_product.py
@@ -67,6 +67,24 @@ def query_collections_with_filter():
     return query
 
 
+@pytest.fixture
+def query_categories_with_filter():
+    query = """
+    query ($filter: CategoryFilterInput!, ) {
+          categories(first:5, filter: $filter) {
+            totalCount
+            edges{
+              node{
+                id
+                name
+              }
+            }
+          }
+        }
+        """
+    return query
+
+
 def test_fetch_all_products(user_api_client, product):
     query = """
     query {
@@ -591,25 +609,15 @@ def test_filter_products_by_collections(user_api_client, collection, product):
     assert product_data["name"] == product.name
 
 
-def test_sort_products(user_api_client, product):
-    # set price and update date of the first product
-    product.price = Money("10.00", "USD")
-    product.minimal_variant_price = Money("10.00", "USD")
-    product.updated_at = datetime.utcnow()
-    product.save()
-
-    # Create the second product with higher price and date
-    product.pk = None
-    product.price = Money("20.00", "USD")
-    product.minimal_variant_price = Money("20.00", "USD")
-    product.updated_at = datetime.utcnow()
-    product.save()
-
-    query = """
+SORT_PRODUCTS_QUERY = """
     query {
         products(sortBy: %(sort_by_product_order)s, first: 2) {
             edges {
                 node {
+                    isPublished
+                    productType{
+                        name
+                    }
                     pricing {
                         priceRangeUndiscounted {
                             start {
@@ -631,7 +639,24 @@ def test_sort_products(user_api_client, product):
             }
         }
     }
-    """
+"""
+
+
+def test_sort_products(user_api_client, product):
+    # set price and update date of the first product
+    product.price = Money("10.00", "USD")
+    product.minimal_variant_price = Money("10.00", "USD")
+    product.updated_at = datetime.utcnow()
+    product.save()
+
+    # Create the second product with higher price and date
+    product.pk = None
+    product.price = Money("20.00", "USD")
+    product.minimal_variant_price = Money("20.00", "USD")
+    product.updated_at = datetime.utcnow()
+    product.save()
+
+    query = SORT_PRODUCTS_QUERY
 
     # Test sorting by PRICE, ascending
     asc_price_query = query % {"sort_by_product_order": "{field: PRICE, direction:ASC}"}
@@ -700,6 +725,62 @@ def test_sort_products(user_api_client, product):
     assert parse_datetime(date_0) > parse_datetime(date_1)
 
 
+def test_sort_products_published(staff_api_client, product, permission_manage_products):
+    # Create the second not published product
+    product.pk = None
+    product.is_published = False
+    product.save()
+
+    staff_api_client.user.user_permissions.add(permission_manage_products)
+
+    # Test sorting by PUBLISHED, ascending
+    asc_published_query = SORT_PRODUCTS_QUERY % {
+        "sort_by_product_order": "{field: PUBLISHED, direction:ASC}"
+    }
+    response = staff_api_client.post_graphql(asc_published_query)
+    content = get_graphql_content(response)
+    is_published_0 = content["data"]["products"]["edges"][0]["node"]["isPublished"]
+    is_published_1 = content["data"]["products"]["edges"][1]["node"]["isPublished"]
+    assert is_published_0 is False
+    assert is_published_1 is True
+
+    # Test sorting by PUBLISHED, descending
+    desc_published_query = SORT_PRODUCTS_QUERY % {
+        "sort_by_product_order": "{field: PUBLISHED, direction:DESC}"
+    }
+    response = staff_api_client.post_graphql(desc_published_query)
+    content = get_graphql_content(response)
+    is_published_0 = content["data"]["products"]["edges"][0]["node"]["isPublished"]
+    is_published_1 = content["data"]["products"]["edges"][1]["node"]["isPublished"]
+    assert is_published_0 is True
+    assert is_published_1 is False
+
+
+def test_sort_products_product_type_name(
+    user_api_client, product, product_with_default_variant
+):
+    # Test sorting by TYPE, ascending
+    asc_published_query = SORT_PRODUCTS_QUERY % {
+        "sort_by_product_order": "{field: TYPE, direction:ASC}"
+    }
+    response = user_api_client.post_graphql(asc_published_query)
+    content = get_graphql_content(response)
+    edges = content["data"]["products"]["edges"]
+    product_type_name_0 = edges[0]["node"]["productType"]["name"]
+    product_type_name_1 = edges[1]["node"]["productType"]["name"]
+    assert product_type_name_0 < product_type_name_1
+
+    # Test sorting by PUBLISHED, descending
+    desc_published_query = SORT_PRODUCTS_QUERY % {
+        "sort_by_product_order": "{field: TYPE, direction:DESC}"
+    }
+    response = user_api_client.post_graphql(desc_published_query)
+    content = get_graphql_content(response)
+    product_type_name_0 = edges[0]["node"]["productType"]["name"]
+    product_type_name_1 = edges[1]["node"]["productType"]["name"]
+    assert product_type_name_0 < product_type_name_1
+
+
 def test_create_product(
     staff_api_client,
     product_type,
@@ -2263,6 +2344,41 @@ def test_collections_query_with_filter(
     assert len(collections) == count
 
 
+@pytest.mark.parametrize(
+    "category_filter, count",
+    [
+        ({"search": "slug_"}, 3),
+        ({"search": "Category1"}, 1),
+        ({"search": "cat1"}, 2),
+        ({"search": "Subcategory_description"}, 1),
+    ],
+)
+def test_categories_query_with_filter(
+    category_filter,
+    count,
+    query_categories_with_filter,
+    staff_api_client,
+    permission_manage_products,
+):
+    Category.objects.create(
+        name="Category1", slug="slug_category1", description="Description cat1"
+    )
+    Category.objects.create(
+        name="Category2", slug="slug_category2", description="Description cat2"
+    )
+    Category.objects.create(
+        name="SubCategory",
+        slug="slug_subcategory",
+        parent=Category.objects.get(name="Category1"),
+        description="Subcategory_description of cat1",
+    )
+    variables = {"filter": category_filter}
+    staff_api_client.user.user_permissions.add(permission_manage_products)
+    response = staff_api_client.post_graphql(query_categories_with_filter, variables)
+    content = get_graphql_content(response)
+    assert content["data"]["categories"]["totalCount"] == count
+
+
 @pytest.mark.parametrize(
     "collection_filter, count",
     [
diff --git a/tests/conftest.py b/tests/conftest.py
index 2b718483fb1..c70469eb969 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -203,6 +203,8 @@ def customer_user(address):  # pylint: disable=W0613
         "password",
         default_billing_address=default_address,
         default_shipping_address=default_address,
+        first_name="Leslie",
+        last_name="Wade",
     )
     user.addresses.add(default_address)
     return user


Code review comments:
diff_hunk: @@ -55,13 +55,27 @@ def filter_created_range(qs, _, value):
     return qs
 
 
+def filter_order_search(qs, _, value):
+    order_fields = [
+        "discount_name",
+        "token",
path: saleor/graphql/order/filters.py
text: @user1:
We should also filter by the order number which is definitely more common to use bu customers than the token.

diff_hunk: @@ -55,13 +55,27 @@ def filter_created_range(qs, _, value):
     return qs
 
 
+def filter_order_search(qs, _, value):
+    order_fields = [
+        "discount_name",
+        "token",
+        "user_email",
+        "user__email",
path: saleor/graphql/order/filters.py
text: @user1:
I think this one maybe not necessary because we should always set `user_email` anyway.

diff_hunk: @@ -139,11 +139,15 @@ def filter_by_period(queryset, period, field_name):
 
 
 def generate_query_argument_description(search_fields):
+    deprecated_info = (
+        "DEPRECATED: Will be removed in Saleor 2.10,"
+        ' use "filter: {search: {}}" instead.\n'
path: saleor/graphql/utils.py
text: @user1:
Playground can render fields nicely if you wrap them with backticks (`) so:
```suggestion
        " use `filter: {search: {}}` instead.\n"
```