You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@beam.apache.org by pa...@apache.org on 2022/10/18 16:31:05 UTC

[beam] branch master updated: [Playground][Frontend] Tags filter for Examples Catalog (#22074) (#23532)

This is an automated email from the ASF dual-hosted git repository.

pabloem pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git


The following commit(s) were added to refs/heads/master by this push:
     new 2e2bb66f82a [Playground][Frontend] Tags filter for Examples Catalog (#22074) (#23532)
2e2bb66f82a is described below

commit 2e2bb66f82a0d9013283b838fae8b118a1e2c652
Author: Darkhan Nausharipov <31...@users.noreply.github.com>
AuthorDate: Tue Oct 18 22:30:54 2022 +0600

    [Playground][Frontend] Tags filter for Examples Catalog (#22074) (#23532)
    
    * [Playground] Tags filter for Examples Catalog (#22074)
    
    * addressing review comments (1)(#22074)
    
    Co-authored-by: darkhan.nausharipov <da...@kzn.akvelon.com>
    Co-authored-by: Alexey Inkin <le...@inkin.ru>
---
 .../examples/components/examples_components.dart   |   5 +-
 .../modules/examples/components/filter/filter.dart |  97 ++++++++++++++++++++
 .../{category_bubble.dart => tag_bubble.dart}      |  16 ++--
 .../{category_bubble.dart => type_bubble.dart}     |   6 +-
 .../examples/components/filter/type_filter.dart    |  59 ------------
 .../components/search_field/search_field.dart      |   6 +-
 .../lib/modules/examples/example_selector.dart     |   2 +-
 .../playground/states/example_selector_state.dart  | 101 ++++++++++++++-------
 .../lib/src/cache/example_cache.dart               |   1 +
 .../example_loaders/content_example_loader.dart    |  11 ++-
 .../example_loaders/empty_example_loader.dart      |   1 +
 .../lib/src/models/example.dart                    |   2 +
 .../lib/src/models/example_base.dart               |   2 +
 .../example_client/grpc_example_client.dart        |   1 +
 .../test/src/common/categories.dart                |  20 ++--
 .../test/src/common/examples.dart                  |   7 +-
 .../states/example_selector_state_test.dart        |  56 +++++++++---
 17 files changed, 255 insertions(+), 138 deletions(-)

diff --git a/playground/frontend/lib/modules/examples/components/examples_components.dart b/playground/frontend/lib/modules/examples/components/examples_components.dart
index cd7ac4ad4f7..c25c30a63e0 100644
--- a/playground/frontend/lib/modules/examples/components/examples_components.dart
+++ b/playground/frontend/lib/modules/examples/components/examples_components.dart
@@ -18,6 +18,7 @@
 
 export 'package:playground/modules/examples/components/example_list/category_expansion_panel.dart';
 export 'package:playground/modules/examples/components/example_list/example_list.dart';
-export 'package:playground/modules/examples/components/filter/category_bubble.dart';
-export 'package:playground/modules/examples/components/filter/type_filter.dart';
+export 'package:playground/modules/examples/components/filter/tag_bubble.dart';
+export 'package:playground/modules/examples/components/filter/type_bubble.dart';
+export 'package:playground/modules/examples/components/filter/filter.dart';
 export 'package:playground/modules/examples/components/search_field/search_field.dart';
diff --git a/playground/frontend/lib/modules/examples/components/filter/filter.dart b/playground/frontend/lib/modules/examples/components/filter/filter.dart
new file mode 100644
index 00000000000..56fa6956abe
--- /dev/null
+++ b/playground/frontend/lib/modules/examples/components/filter/filter.dart
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:flutter/material.dart';
+import 'package:playground/constants/sizes.dart';
+import 'package:playground/modules/examples/components/examples_components.dart';
+import 'package:playground/pages/playground/states/example_selector_state.dart';
+import 'package:playground_components/playground_components.dart';
+import 'package:provider/provider.dart';
+
+class ExamplesFilter extends StatelessWidget {
+  const ExamplesFilter({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.symmetric(
+        horizontal: kLgSpacing,
+        vertical: kMdSpacing,
+      ),
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: const [
+          _Types(),
+          _Tags(),
+        ],
+      ),
+    );
+  }
+}
+
+class _Types extends StatelessWidget {
+  const _Types();
+
+  @override
+  Widget build(BuildContext context) {
+    AppLocalizations appLocale = AppLocalizations.of(context)!;
+
+    return Row(
+      children: [
+        TypeBubble(
+          type: ExampleType.all,
+          name: appLocale.all,
+        ),
+        TypeBubble(
+          type: ExampleType.example,
+          name: appLocale.examples,
+        ),
+        TypeBubble(
+          type: ExampleType.kata,
+          name: appLocale.katas,
+        ),
+        TypeBubble(
+          type: ExampleType.test,
+          name: appLocale.unitTests,
+        ),
+      ],
+    );
+  }
+}
+
+class _Tags extends StatelessWidget {
+  const _Tags();
+
+  @override
+  Widget build(BuildContext context) {
+    return Consumer<ExampleSelectorState>(
+      builder: (context, state, child) => Padding(
+        padding: const EdgeInsets.symmetric(vertical: kMdSpacing),
+        child: SingleChildScrollView(
+          scrollDirection: Axis.horizontal,
+          child: Row(
+            children: state.tags
+                .map((tag) => TagBubble(name: tag))
+                .toList(growable: false),
+          ),
+        ),
+      ),
+    );
+  }
+}
diff --git a/playground/frontend/lib/modules/examples/components/filter/category_bubble.dart b/playground/frontend/lib/modules/examples/components/filter/tag_bubble.dart
similarity index 82%
copy from playground/frontend/lib/modules/examples/components/filter/category_bubble.dart
copy to playground/frontend/lib/modules/examples/components/filter/tag_bubble.dart
index 849f19db877..f4768e0c533 100644
--- a/playground/frontend/lib/modules/examples/components/filter/category_bubble.dart
+++ b/playground/frontend/lib/modules/examples/components/filter/tag_bubble.dart
@@ -21,13 +21,11 @@ import 'package:playground/pages/playground/states/example_selector_state.dart';
 import 'package:playground_components/playground_components.dart';
 import 'package:provider/provider.dart';
 
-class CategoryBubble extends StatelessWidget {
-  final ExampleType type;
+class TagBubble extends StatelessWidget {
   final String name;
 
-  const CategoryBubble({
+  const TagBubble({
     Key? key,
-    required this.type,
     required this.name,
   }) : super(key: key);
 
@@ -35,16 +33,18 @@ class CategoryBubble extends StatelessWidget {
   Widget build(BuildContext context) {
     return Consumer<ExampleSelectorState>(
       builder: (context, state, child) {
-        final isSelected = type == state.selectedFilterType;
+        final isSelected = state.selectedTags.contains(name);
 
         return BubbleWidget(
           isSelected: isSelected,
           title: name,
           onTap: () {
-            if (!isSelected) {
-              state.setSelectedFilterType(type);
-              state.sortCategories();
+            if (isSelected) {
+              state.removeSelectedTag(name);
+            } else {
+              state.addSelectedTag(name);
             }
+            state.filterCategoriesWithExamples();
           },
         );
       },
diff --git a/playground/frontend/lib/modules/examples/components/filter/category_bubble.dart b/playground/frontend/lib/modules/examples/components/filter/type_bubble.dart
similarity index 93%
rename from playground/frontend/lib/modules/examples/components/filter/category_bubble.dart
rename to playground/frontend/lib/modules/examples/components/filter/type_bubble.dart
index 849f19db877..2925d54f336 100644
--- a/playground/frontend/lib/modules/examples/components/filter/category_bubble.dart
+++ b/playground/frontend/lib/modules/examples/components/filter/type_bubble.dart
@@ -21,11 +21,11 @@ import 'package:playground/pages/playground/states/example_selector_state.dart';
 import 'package:playground_components/playground_components.dart';
 import 'package:provider/provider.dart';
 
-class CategoryBubble extends StatelessWidget {
+class TypeBubble extends StatelessWidget {
   final ExampleType type;
   final String name;
 
-  const CategoryBubble({
+  const TypeBubble({
     Key? key,
     required this.type,
     required this.name,
@@ -43,7 +43,7 @@ class CategoryBubble extends StatelessWidget {
           onTap: () {
             if (!isSelected) {
               state.setSelectedFilterType(type);
-              state.sortCategories();
+              state.filterCategoriesWithExamples();
             }
           },
         );
diff --git a/playground/frontend/lib/modules/examples/components/filter/type_filter.dart b/playground/frontend/lib/modules/examples/components/filter/type_filter.dart
deleted file mode 100644
index a221999f3ce..00000000000
--- a/playground/frontend/lib/modules/examples/components/filter/type_filter.dart
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import 'package:flutter/material.dart';
-import 'package:flutter_gen/gen_l10n/app_localizations.dart';
-import 'package:playground/constants/sizes.dart';
-import 'package:playground/modules/examples/components/examples_components.dart';
-import 'package:playground_components/playground_components.dart';
-
-class TypeFilter extends StatelessWidget {
-  const TypeFilter({Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    AppLocalizations appLocale = AppLocalizations.of(context)!;
-
-    return Padding(
-      padding: const EdgeInsets.symmetric(
-        horizontal: kLgSpacing,
-        vertical: kMdSpacing,
-      ),
-      child: Row(
-        children: <CategoryBubble>[
-          CategoryBubble(
-            type: ExampleType.all,
-            name: appLocale.all,
-          ),
-          CategoryBubble(
-            type: ExampleType.example,
-            name: appLocale.examples,
-          ),
-          CategoryBubble(
-            type: ExampleType.kata,
-            name: appLocale.katas,
-          ),
-          CategoryBubble(
-            type: ExampleType.test,
-            name: appLocale.unitTests,
-          ),
-        ],
-      ),
-    );
-  }
-}
diff --git a/playground/frontend/lib/modules/examples/components/search_field/search_field.dart b/playground/frontend/lib/modules/examples/components/search_field/search_field.dart
index 2095dfac1d2..2ca7a354c3f 100644
--- a/playground/frontend/lib/modules/examples/components/search_field/search_field.dart
+++ b/playground/frontend/lib/modules/examples/components/search_field/search_field.dart
@@ -89,8 +89,8 @@ class SearchField extends StatelessWidget {
     );
   }
 
-  _onChange(ExampleSelectorState state, String filterText) {
-    state.setFilterText(filterText);
-    state.sortCategories();
+  _onChange(ExampleSelectorState state, String searchText) {
+    state.setSearchText(searchText);
+    state.filterCategoriesWithExamples();
   }
 }
diff --git a/playground/frontend/lib/modules/examples/example_selector.dart b/playground/frontend/lib/modules/examples/example_selector.dart
index c08fdb3cea2..45b936fb9a1 100644
--- a/playground/frontend/lib/modules/examples/example_selector.dart
+++ b/playground/frontend/lib/modules/examples/example_selector.dart
@@ -190,7 +190,7 @@ class _ExampleSelectorState extends State<ExampleSelector>
     return Column(
       children: [
         SearchField(controller: textController),
-        const TypeFilter(),
+        const ExamplesFilter(),
         ExampleList(
           controller: scrollController,
           selectedExample: playgroundController.selectedExample!,
diff --git a/playground/frontend/lib/pages/playground/states/example_selector_state.dart b/playground/frontend/lib/pages/playground/states/example_selector_state.dart
index f62a1773012..7ce9064e1b9 100644
--- a/playground/frontend/lib/pages/playground/states/example_selector_state.dart
+++ b/playground/frontend/lib/pages/playground/states/example_selector_state.dart
@@ -22,27 +22,57 @@ import 'package:playground_components/playground_components.dart';
 class ExampleSelectorState with ChangeNotifier {
   final PlaygroundController _playgroundController;
   ExampleType _selectedFilterType;
-  String _filterText;
+  String _searchText;
   List<CategoryWithExamples> categories;
+  List<String> tags = [];
+  List<String> selectedTags = [];
 
   ExampleSelectorState(
     this._playgroundController,
     this.categories, [
     this._selectedFilterType = ExampleType.all,
-    this._filterText = '',
-  ]);
+    this._searchText = '',
+  ]) {
+    tags = _getTagsSortedByExampleCount(categories);
+  }
 
   ExampleType get selectedFilterType => _selectedFilterType;
 
-  String get filterText => _filterText;
+  String get searchText => _searchText;
 
   void setSelectedFilterType(ExampleType type) {
     _selectedFilterType = type;
     notifyListeners();
   }
 
-  void setFilterText(String text) {
-    _filterText = text;
+  void addSelectedTag(String tag) {
+    selectedTags.add(tag);
+    notifyListeners();
+  }
+
+  void removeSelectedTag(String tag) {
+    selectedTags.remove(tag);
+    notifyListeners();
+  }
+
+  List<String> _getTagsSortedByExampleCount(
+    List<CategoryWithExamples> categories,
+  ) {
+    Map<String, int> exampleCountByTag = {};
+    for (final category in categories) {
+      for (final example in category.examples) {
+        for (final tag in example.tags) {
+          exampleCountByTag[tag] = (exampleCountByTag[tag] ?? 0) + 1;
+        }
+      }
+    }
+    final tagEntries = exampleCountByTag.entries.toList()
+      ..sort((entry1, entry2) => entry2.value.compareTo(entry1.value));
+    return tagEntries.map((entry) => entry.key).toList();
+  }
+
+  void setSearchText(String text) {
+    _searchText = text;
     notifyListeners();
   }
 
@@ -51,56 +81,59 @@ class ExampleSelectorState with ChangeNotifier {
     notifyListeners();
   }
 
-  void sortCategories() {
+  void filterCategoriesWithExamples() {
     final categories = _playgroundController.exampleCache.getCategories(
       _playgroundController.sdk,
     );
-
-    final sortedCategories = categories
+    final filteredCategories = categories
         .map((category) => CategoryWithExamples(
             title: category.title,
-            examples: _sortCategoryExamples(category.examples)))
+            examples: _filterExamples(category.examples)))
         .where((category) => category.examples.isNotEmpty)
         .toList();
-    setCategories(sortedCategories);
+    setCategories(filteredCategories);
+  }
+
+  List<ExampleBase> _filterExamples(List<ExampleBase> examples) {
+    final byType = filterExamplesByType(examples, selectedFilterType);
+    final byTags = filterExamplesByTags(byType);
+    final byName = filterExamplesByName(byTags);
+    return byName;
   }
 
-  List<ExampleBase> _sortCategoryExamples(List<ExampleBase> examples) {
-    final isAllFilterType = selectedFilterType == ExampleType.all;
-    final isFilterTextEmpty = filterText.isEmpty;
-    if (isAllFilterType && isFilterTextEmpty) {
+  @visibleForTesting
+  List<ExampleBase> filterExamplesByTags(List<ExampleBase> examples) {
+    if (selectedTags.isEmpty) {
       return examples;
     }
-    if (!isAllFilterType && isFilterTextEmpty) {
-      return sortExamplesByType(
-        examples,
-        selectedFilterType,
-      );
+    List<ExampleBase> sorted = [];
+    for (var example in examples) {
+      if (example.tags.toSet().containsAll(selectedTags)) {
+        sorted.add(example);
+      }
     }
-    if (isAllFilterType && !isFilterTextEmpty) {
-      return sortExamplesByName(examples, filterText);
-    }
-    final sorted = sortExamplesByType(
-      examples,
-      selectedFilterType,
-    );
-    return sortExamplesByName(sorted, filterText);
+    return sorted;
   }
 
-  List<ExampleBase> sortExamplesByType(
+  @visibleForTesting
+  List<ExampleBase> filterExamplesByType(
     List<ExampleBase> examples,
     ExampleType type,
   ) {
+    if (type == ExampleType.all) {
+      return examples;
+    }
     return examples.where((element) => element.type == type).toList();
   }
 
-  List<ExampleBase> sortExamplesByName(
-    List<ExampleBase> examples,
-    String name,
-  ) {
+  @visibleForTesting
+  List<ExampleBase> filterExamplesByName(List<ExampleBase> examples) {
+    if (_searchText.isEmpty) {
+      return examples;
+    }
     return examples
         .where((example) =>
-            example.name.toLowerCase().contains(name.toLowerCase()))
+            example.name.toLowerCase().contains(_searchText.toLowerCase()))
         .toList();
   }
 }
diff --git a/playground/frontend/playground_components/lib/src/cache/example_cache.dart b/playground/frontend/playground_components/lib/src/cache/example_cache.dart
index 5e600591fe9..032d4fcbce9 100644
--- a/playground/frontend/playground_components/lib/src/cache/example_cache.dart
+++ b/playground/frontend/playground_components/lib/src/cache/example_cache.dart
@@ -114,6 +114,7 @@ class ExampleCache extends ChangeNotifier {
       name: result.files.first.name,
       path: id,
       description: '',
+      tags: [],
       type: ExampleType.example,
       source: result.files.first.code,
       pipelineOptions: result.pipelineOptions,
diff --git a/playground/frontend/playground_components/lib/src/controllers/example_loaders/content_example_loader.dart b/playground/frontend/playground_components/lib/src/controllers/example_loaders/content_example_loader.dart
index a66e9fa69ad..8196ff10b8b 100644
--- a/playground/frontend/playground_components/lib/src/controllers/example_loaders/content_example_loader.dart
+++ b/playground/frontend/playground_components/lib/src/controllers/example_loaders/content_example_loader.dart
@@ -33,13 +33,14 @@ class ContentExampleLoader extends ExampleLoader {
 
   @override
   Future<Example> get future async => Example(
-        sdk: descriptor.sdk,
+        complexity: descriptor.complexity,
+        description: '',
         name: descriptor.name ?? 'Embedded_Example',
         path: '',
-        description: '',
-        type: ExampleType.example,
-        source: descriptor.content,
         pipelineOptions: '',
-        complexity: descriptor.complexity,
+        sdk: descriptor.sdk,
+        source: descriptor.content,
+        tags: [],
+        type: ExampleType.example,
       );
 }
diff --git a/playground/frontend/playground_components/lib/src/controllers/example_loaders/empty_example_loader.dart b/playground/frontend/playground_components/lib/src/controllers/example_loaders/empty_example_loader.dart
index 0303444c76a..faef4c0098f 100644
--- a/playground/frontend/playground_components/lib/src/controllers/example_loaders/empty_example_loader.dart
+++ b/playground/frontend/playground_components/lib/src/controllers/example_loaders/empty_example_loader.dart
@@ -38,6 +38,7 @@ class EmptyExampleLoader extends ExampleLoader {
         name: 'Embedded_Example',
         path: '',
         description: '',
+        tags: [],
         type: ExampleType.example,
         source: '',
         pipelineOptions: '',
diff --git a/playground/frontend/playground_components/lib/src/models/example.dart b/playground/frontend/playground_components/lib/src/models/example.dart
index 2c072d100b2..2654ecb6461 100644
--- a/playground/frontend/playground_components/lib/src/models/example.dart
+++ b/playground/frontend/playground_components/lib/src/models/example.dart
@@ -27,6 +27,7 @@ class Example extends ExampleBase {
 
   const Example({
     required super.sdk,
+    required super.tags,
     required super.type,
     required super.name,
     required super.path,
@@ -53,6 +54,7 @@ class Example extends ExampleBase {
           name: example.name,
           path: example.path,
           description: example.description,
+          tags: example.tags,
           type: example.type,
           contextLine: example.contextLine,
           isMultiFile: example.isMultiFile,
diff --git a/playground/frontend/playground_components/lib/src/models/example_base.dart b/playground/frontend/playground_components/lib/src/models/example_base.dart
index 10a55e1be0b..62968579b27 100644
--- a/playground/frontend/playground_components/lib/src/models/example_base.dart
+++ b/playground/frontend/playground_components/lib/src/models/example_base.dart
@@ -49,6 +49,7 @@ extension ExampleTypeToString on ExampleType {
 /// These objects are fetched as lists from [ExampleRepository].
 class ExampleBase with Comparable<ExampleBase>, EquatableMixin {
   final Sdk sdk;
+  final List<String> tags;
   final ExampleType type;
   final String name;
   final String path;
@@ -64,6 +65,7 @@ class ExampleBase with Comparable<ExampleBase>, EquatableMixin {
     required this.name,
     required this.path,
     required this.description,
+    required this.tags,
     required this.type,
     this.contextLine = 1,
     this.isMultiFile = false,
diff --git a/playground/frontend/playground_components/lib/src/repositories/example_client/grpc_example_client.dart b/playground/frontend/playground_components/lib/src/repositories/example_client/grpc_example_client.dart
index 6292fcb77c5..db286d31593 100644
--- a/playground/frontend/playground_components/lib/src/repositories/example_client/grpc_example_client.dart
+++ b/playground/frontend/playground_components/lib/src/repositories/example_client/grpc_example_client.dart
@@ -334,6 +334,7 @@ class GrpcExampleClient implements ExampleClient {
       sdk: sdk,
       name: example.name,
       description: example.description,
+      tags: example.tags,
       type: _exampleTypeFromString(example.type),
       path: example.cloudPath,
       contextLine: example.contextLine,
diff --git a/playground/frontend/playground_components/test/src/common/categories.dart b/playground/frontend/playground_components/test/src/common/categories.dart
index 39c0f4c38bc..2bba911d19c 100644
--- a/playground/frontend/playground_components/test/src/common/categories.dart
+++ b/playground/frontend/playground_components/test/src/common/categories.dart
@@ -24,19 +24,25 @@ import 'package:playground_components/src/models/sdk.dart';
 import 'examples.dart';
 
 final categoriesMock = [
-  CategoryWithExamples(title: 'Sorted', examples: [exampleMock1]),
-  CategoryWithExamples(title: 'Unsorted', examples: [exampleMock2]),
+  CategoryWithExamples(title: 'Filtered', examples: [exampleMock1]),
+  CategoryWithExamples(
+    title: 'Unfiltered',
+    // exampleMock2 is repeated to test that 'tag2' is more frequent than 'tag1'
+    examples: [exampleMock1, exampleMock2, exampleMock2, exampleMock2],
+  ),
 ];
 
-final sortedCategories = [
-  CategoryWithExamples(title: 'Sorted', examples: [exampleMock1]),
+final filteredCategories = [
+  CategoryWithExamples(title: 'Filtered', examples: [exampleMock1]),
 ];
 
-const unsortedExamples = [exampleMock1, exampleMock2];
+const filteredExamples = [exampleMock1, exampleMock2];
 
-const examplesSortedByTypeMock = [exampleMock2];
+const examplesFilteredByTypeMock = [exampleMock2];
 
-const examplesSortedByNameMock = [exampleMock1];
+const examplesFilteredByTagsMock = [exampleMock2];
+
+const examplesFilteredByNameMock = [exampleMock1];
 
 final sdkCategoriesFromServerMock = UnmodifiableMapView({
   Sdk.java: categoriesMock,
diff --git a/playground/frontend/playground_components/test/src/common/examples.dart b/playground/frontend/playground_components/test/src/common/examples.dart
index e8590e87b36..ec2e82fd24d 100644
--- a/playground/frontend/playground_components/test/src/common/examples.dart
+++ b/playground/frontend/playground_components/test/src/common/examples.dart
@@ -24,7 +24,8 @@ import 'package:playground_components/src/models/sdk.dart';
 const exampleMock1 = Example(
   sdk: Sdk.python,
   source: 'ex1',
-  name: 'Example',
+  name: 'Example X1',
+  tags: ['tag1'],
   type: ExampleType.example,
   description: 'description',
   path: 'SDK_PYTHON/Category/Name',
@@ -36,6 +37,7 @@ const exampleMock2 = Example(
   sdk: Sdk.python,
   source: 'ex2',
   name: 'Kata',
+  tags: ['tag2'],
   type: ExampleType.kata,
   description: 'description',
   path: 'SDK_PYTHON/Category/Name',
@@ -46,6 +48,7 @@ const exampleMock2 = Example(
 const exampleWithoutSourceMock = ExampleBase(
   sdk: Sdk.python,
   name: 'Test example',
+  tags: [],
   type: ExampleType.example,
   description: 'description',
   path: 'SDK_PYTHON/Category/Name',
@@ -56,6 +59,7 @@ const exampleWithoutSourceMock = ExampleBase(
 const exampleWithAllAdditionsMock = Example(
   sdk: Sdk.python,
   name: 'Test example',
+  tags: [],
   type: ExampleType.example,
   description: 'description',
   path: 'SDK_PYTHON/Category/Name',
@@ -71,6 +75,7 @@ const exampleMockGo = Example(
   sdk: Sdk.go,
   source: 'ex1',
   name: 'Example',
+  tags: [],
   type: ExampleType.example,
   description: 'description',
   path: 'SDK_GO/Category/Name',
diff --git a/playground/frontend/test/pages/playground/states/example_selector_state_test.dart b/playground/frontend/test/pages/playground/states/example_selector_state_test.dart
index 1d1c37d4d95..e31dee00cab 100644
--- a/playground/frontend/test/pages/playground/states/example_selector_state_test.dart
+++ b/playground/frontend/test/pages/playground/states/example_selector_state_test.dart
@@ -58,7 +58,7 @@ void main() {
   test(
     'ExampleSelector state filterText should be empty string by default',
     () {
-      expect(state.filterText, '');
+      expect(state.searchText, '');
     },
   );
 
@@ -76,9 +76,9 @@ void main() {
     'ExampleSelector state should notify all listeners about filterText change',
     () {
       state.addListener(() {
-        expect(state.filterText, 'test');
+        expect(state.searchText, 'test');
       });
-      state.setFilterText('test');
+      state.setSearchText('test');
     },
   );
 
@@ -93,7 +93,7 @@ void main() {
   );
 
   test(
-      'ExampleSelector state sortCategories should:'
+      'ExampleSelector state filterCategories should:'
       '- update categories and notify all listeners,'
       'but should NOT:'
       '- affect Example state categories', () {
@@ -101,11 +101,11 @@ void main() {
       expect(state.categories, []);
       expect(exampleCache.categoryListsBySdk, exampleCache.categoryListsBySdk);
     });
-    state.sortCategories();
+    state.filterCategoriesWithExamples();
   });
 
   test(
-      'ExampleSelector state sortExamplesByType should:'
+      'ExampleSelector state filterExamplesByType should:'
       '- update categories,'
       '- notify all listeners,'
       'but should NOT:'
@@ -115,15 +115,32 @@ void main() {
       categoriesMock,
     );
     state.addListener(() {
-      expect(state.categories, examplesSortedByTypeMock);
+      expect(state.categories, examplesFilteredByTypeMock);
       expect(exampleCache.categoryListsBySdk, exampleCache.categoryListsBySdk);
     });
-    state.sortExamplesByType(unsortedExamples, ExampleType.kata);
+    state.filterExamplesByType(filteredExamples, ExampleType.kata);
   });
 
   test(
-      'ExampleSelector state sortExamplesByName should:'
-      '- update categories'
+      'ExampleSelector state filterExamplesByTags should:'
+      '- return examples which contain all selected tags'
+      '- notify all listeners,'
+      'but should NOT:'
+      '- affect Example state categories', () {
+    final state = ExampleSelectorState(
+      playgroundController,
+      categoriesMock,
+    );
+    state.addSelectedTag('tag2');
+    expect(
+      state.filterExamplesByTags(filteredExamples),
+      examplesFilteredByTagsMock,
+    );
+  });
+
+  test(
+      'ExampleSelector state filterExamplesByName should:'
+      '- return examples with matching names'
       '- notify all listeners,'
       'but should NOT:'
       '- wait for full name of example,'
@@ -133,10 +150,19 @@ void main() {
       playgroundController,
       categoriesMock,
     );
-    state.addListener(() {
-      expect(state.categories, examplesSortedByNameMock);
-      expect(exampleCache.categoryListsBySdk, exampleCache.categoryListsBySdk);
-    });
-    state.sortExamplesByName(unsortedExamples, 'X1');
+    state.setSearchText('Example X1');
+    expect(
+      state.filterExamplesByName(filteredExamples),
+      examplesFilteredByNameMock,
+    );
+  });
+
+  test('ExampleSelectorState sorts tags by example count', () {
+    final state = ExampleSelectorState(
+      playgroundController,
+      categoriesMock,
+    );
+    const popularTag = 'tag2';
+    expect(state.tags.first == popularTag, true);
   });
 }