From 84fd9af333554eb2e0c88c5073f544504a34eb54 Mon Sep 17 00:00:00 2001 From: helderbetiol <37706737+helderbetiol@users.noreply.github.com> Date: Wed, 19 Jun 2024 19:15:52 +0200 Subject: [PATCH] feat(app) add complex filter --- APP/lib/common/api_backend.dart | 35 ++++- APP/lib/l10n/app_en.arb | 5 + APP/lib/l10n/app_es.arb | 5 + APP/lib/l10n/app_fr.arb | 13 ++ APP/lib/l10n/app_pt.arb | 5 + .../widgets/select_objects/object_popup.dart | 7 +- .../select_objects/select_objects.dart | 17 ++- .../settings_view/_advanced_find_field.dart | 126 ++++++++++++++++++ .../settings_view/_find_node_field.dart | 9 +- .../select_objects/settings_view/_header.dart | 35 +---- .../settings_view/settings_view.dart | 17 ++- .../settings_view/tree_filter.dart | 3 +- .../select_objects/treeapp_controller.dart | 16 ++- .../select_objects/view_object_popup.dart | 4 +- APP/lib/widgets/tenants/domain_view.dart | 5 +- APP/test/select_objects_test.dart | 4 +- 16 files changed, 242 insertions(+), 64 deletions(-) create mode 100644 APP/lib/widgets/select_objects/settings_view/_advanced_find_field.dart diff --git a/APP/lib/common/api_backend.dart b/APP/lib/common/api_backend.dart index 8bbc1b717..b1152b9c9 100644 --- a/APP/lib/common/api_backend.dart +++ b/APP/lib/common/api_backend.dart @@ -13,6 +13,7 @@ import 'package:ogree_app/models/tag.dart'; import 'package:ogree_app/models/tenant.dart'; import 'package:ogree_app/models/user.dart'; import 'package:universal_html/html.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'definitions.dart'; @@ -434,7 +435,8 @@ Future> createObject( } } -Future, Exception>> fetchObject(String id, +Future, Exception>> fetchObject( + String id, AppLocalizations localeMsg, {String idKey = "id"}) async { print("API fetch Object"); try { @@ -444,7 +446,7 @@ Future, Exception>> fetchObject(String id, Map data = json.decode(response.body); var list = List>.from(data["data"]); if (list.isEmpty) { - return Failure(Exception("No object found for to this request")); + return Failure(Exception(localeMsg.noObjectsFound)); } return Success(list.first); } else { @@ -474,6 +476,31 @@ Future, Exception>> fetchObjectChildren( } } +Future>, Exception>> fetchWithComplexFilter( + String filter, Namespace namespace, AppLocalizations localeMsg) async { + print("API fetch Complex Filter"); + try { + Uri url = Uri.parse( + '$apiUrl/api/objects/search?namespace=${namespace.name.toLowerCase()}'); + final response = await http.post(url, + body: json.encode({'filter': filter}), + headers: getHeader(token)); + if (response.statusCode == 200 || response.statusCode == 201) { + Map data = json.decode(response.body); + var list = List>.from(data["data"]); + if (list.isEmpty) { + return Failure(Exception(localeMsg.noObjectsFound)); + } + return Success(list); + } else { + final Map data = json.decode(response.body); + return Failure(Exception(data["message"].toString())); + } + } on Exception catch (e) { + return Failure(e); + } +} + Future> updateObject( String objId, String category, Map object) async { print("API update object"); @@ -541,7 +568,7 @@ Future> createTemplate( } Future, Exception>> fetchGroupContent( - String id, category) async { + String id, category, AppLocalizations localeMsg) async { print("API fetch GR content"); try { Uri url = Uri.parse('$apiUrl/api/objects?id=$id.*&category=$category'); @@ -551,7 +578,7 @@ Future, Exception>> fetchGroupContent( var list = List>.from(data["data"]); print(list); if (list.isEmpty) { - return Failure(Exception("No object found for to this request")); + return Failure(Exception(localeMsg.noObjectsFound)); } else { List content = []; for (var item in list) { diff --git a/APP/lib/l10n/app_en.arb b/APP/lib/l10n/app_en.arb index 2075e48a5..3f3e639e8 100644 --- a/APP/lib/l10n/app_en.arb +++ b/APP/lib/l10n/app_en.arb @@ -65,16 +65,21 @@ "whatNamespace": "Select a namespace:", "searchById": "Search by ID", + "searchAdvanced": "Advanced search", "filters": "Filters", + "categoryFilters": "Filter ID by category", "clearAllFilters": "Clear all", "noNodeFound": "No node found with ID:", "nodeFound": "Node found:", + "xNodesFound": "{count, plural, =1{1 node found for:} other{{count} nodes found for:}}", "expandAll" : "Expand All", "collapseAll": "Collapse All", "selectAll": "Select All", "deselectAll": "Deselect All", "nodePath": "Node path:", "toggleSelection": "Toggle selection for this node and all its children", + "expression": "Expression", + "advancedSearchHint": "Compose complex boolean expressions with the operators =, !=, <, <=, >, >=, & and |.\nParenthesis can also be used to separate the expressions.\nExample:", "addColumnTip": "Add a column", "yourReport": "Your report", diff --git a/APP/lib/l10n/app_es.arb b/APP/lib/l10n/app_es.arb index 51ce0b953..f076ea09d 100644 --- a/APP/lib/l10n/app_es.arb +++ b/APP/lib/l10n/app_es.arb @@ -65,9 +65,12 @@ "whatNamespace": "Seleccionar un namespace:", "searchById": "Buscar por ID", + "searchAdvanced": "Búsqueda avanzada", "filters": "Filtros", + "categoryFilters": "Filtros de ID por categoría", "clearAllFilters": "Borrar todo", "noNodeFound": "No se encontró ningún nodo con ID:", + "xNodesFound": "{count, plural, =1{1 nodo encontrado para:} other{{count} nodos encontrados para:}}", "nodeFound": "Nodo encontrado:", "expandAll" : "Expandir Todo", "collapseAll": "Reducir Todo", @@ -75,6 +78,8 @@ "deselectAll": "Deseleccionar Todo", "nodePath": "Ruta del nodo:", "toggleSelection": "Invertir selección de este nodo y todos sus hijos", + "expression": "Expresión", + "advancedSearchHint": "Componga expresiones booleanas complejas con los operadores =, !=, <, <=, >, >=, & y |.\nTambién se pueden utilizar paréntesis para separar las expresiones.\nEjemplo:", "addColumnTip": "Agregar una columna", "yourReport": "Su reporte", diff --git a/APP/lib/l10n/app_fr.arb b/APP/lib/l10n/app_fr.arb index eb192e434..cf6327100 100644 --- a/APP/lib/l10n/app_fr.arb +++ b/APP/lib/l10n/app_fr.arb @@ -76,16 +76,29 @@ "whatNamespace": "Sélectionnez le type de namespace :", "searchById": "Rechercher par ID", + "searchAdvanced": "Recherche avancée", "filters": "Filtres", + "categoryFilters": "Filtrer l'ID par categorie", "clearAllFilters": "Effacer tout", "noNodeFound": "Pas de noeud trouvé avec l'ID :", "nodeFound": "Noeud trouvé :", + "xNodesFound": "{count, plural, =1{1 noeud trouvé pour :} other{{count} noeud trouvés pour :}}", + "@xNodesFound": { + "placeholders": { + "count": { + "type": "num", + "format": "compact" + } + } + }, "expandAll" : "Développer tout", "collapseAll": "Réduire tout", "selectAll": "Sélectionner tout", "deselectAll": "Désélectionner tout", "nodePath": "Chemin du noeud :", "toggleSelection": "Inverser sélection du noeud et de tous ses enfants", + "expression": "Expression", + "advancedSearchHint": "Composer des expressions booléennes complexes avec les opérateurs =, !=, <, <=, >, >=, & et |.\nLa parenthèse peut également être utilisée pour séparer les expressions.\nExemple :", "@result": { "description": "Result Page" }, "result": "Résultat", diff --git a/APP/lib/l10n/app_pt.arb b/APP/lib/l10n/app_pt.arb index 98bedc6db..3a2ca4052 100644 --- a/APP/lib/l10n/app_pt.arb +++ b/APP/lib/l10n/app_pt.arb @@ -65,16 +65,21 @@ "whatNamespace": "Selecione um namespace:", "searchById": "Pesquisar por ID", + "searchAdvanced": "Pesquisa avançada", "filters": "Filtros", + "categoryFilters": "Filtrar ID por categoria", "clearAllFilters": "Limpar tudo", "noNodeFound": "Nenhum nó encontrado com ID:", "nodeFound": "Nó encontrado:", + "xNodesFound": "{count, plural, =1{1 nó encontrado para:} other{{count} nós encontrados para:}}", "expandAll" : "Expandir Todos", "collapseAll": "Recolher Todos", "selectAll": "Selecionar Todos", "deselectAll": "Desmarcar Todos", "nodePath": "Caminho do nó:", "toggleSelection": "Inverter seleção deste nó e de todos os seus filhos", + "expression": "Expressão", + "advancedSearchHint": "Componha expressões booleanas complexas com os operadores =, !=, <, <=, >, >=, & e |.\nParênteses também podem ser usados para separar as expressões.\nExemplo:", "addColumnTip": "Adicionar uma coluna", "yourReport": "Seu relatório", diff --git a/APP/lib/widgets/select_objects/object_popup.dart b/APP/lib/widgets/select_objects/object_popup.dart index 98d4cfa89..deaff865b 100644 --- a/APP/lib/widgets/select_objects/object_popup.dart +++ b/APP/lib/widgets/select_objects/object_popup.dart @@ -457,7 +457,8 @@ class _ObjectPopupState extends State { } Future?> getGroupContent(String parentId, targetCategory) async { - var result = await fetchGroupContent(parentId, targetCategory); + var result = await fetchGroupContent( + parentId, targetCategory, AppLocalizations.of(context)!); switch (result) { case Success(value: final value): return value; @@ -472,7 +473,8 @@ class _ObjectPopupState extends State { var errMsg = ""; // Try both id and slug since we dont know the obj's category for (var keyId in ["id", "slug", "name"]) { - var result = await fetchObject(_objId, idKey: keyId); + var result = await fetchObject(_objId, AppLocalizations.of(context)!, + idKey: keyId); switch (result) { case Success(value: final value): if (widget.namespace == Namespace.Logical) { @@ -581,6 +583,7 @@ class _ObjectPopupState extends State { return TextFormField( controller: textEditingController, focusNode: focusNode, + style: const TextStyle(fontSize: 14), decoration: GetFormInputDecoration( false, "$starSymbol${AppLocalizations.of(context)!.domain}", icon: Icons.edit), diff --git a/APP/lib/widgets/select_objects/select_objects.dart b/APP/lib/widgets/select_objects/select_objects.dart index 28978ace5..781ea629f 100644 --- a/APP/lib/widgets/select_objects/select_objects.dart +++ b/APP/lib/widgets/select_objects/select_objects.dart @@ -75,7 +75,6 @@ class _SelectObjectsState extends State { ]) : _ResponsiveBody( namespace: widget.namespace, - noFilters: widget.namespace != Namespace.Physical, controller: appController, callback: () => setState(() { widget.load = true; @@ -107,14 +106,12 @@ class _Unfocus extends StatelessWidget { class _ResponsiveBody extends StatelessWidget { final Namespace namespace; - final bool noFilters; final TreeAppController controller; final Function() callback; const _ResponsiveBody( {Key? key, required this.namespace, required this.controller, - this.noFilters = false, required this.callback}) : super(key: key); @@ -135,6 +132,7 @@ class _ResponsiveBody extends StatelessWidget { context, SettingsViewPopup( controller: controller, + namespace: namespace, ), isDismissible: true), icon: const Icon(Icons.filter_alt_outlined), @@ -163,7 +161,7 @@ class _ResponsiveBody extends StatelessWidget { color: Colors.black26, ), Expanded( - child: SettingsView(isTenantMode: false, noFilters: noFilters)), + child: SettingsView(isTenantMode: false, namespace: namespace)), ], ), ); @@ -203,8 +201,10 @@ addObjectButton( class SettingsViewPopup extends StatelessWidget { final TreeAppController controller; + final Namespace namespace; - const SettingsViewPopup({super.key, required this.controller}); + const SettingsViewPopup( + {super.key, required this.controller, required this.namespace}); @override Widget build(BuildContext context) { @@ -226,9 +226,12 @@ class SettingsViewPopup extends StatelessWidget { padding: EdgeInsets.zero, shrinkWrap: true, children: [ - const SizedBox( + SizedBox( height: 420, - child: SettingsView(isTenantMode: false), + child: SettingsView( + isTenantMode: false, + namespace: namespace, + ), ), const SizedBox(height: 10), TextButton.icon( diff --git a/APP/lib/widgets/select_objects/settings_view/_advanced_find_field.dart b/APP/lib/widgets/select_objects/settings_view/_advanced_find_field.dart new file mode 100644 index 000000000..d4fb8bca2 --- /dev/null +++ b/APP/lib/widgets/select_objects/settings_view/_advanced_find_field.dart @@ -0,0 +1,126 @@ +part of 'settings_view.dart'; + +class _AdvancedFindField extends StatefulWidget { + final Namespace namespace; + const _AdvancedFindField({required this.namespace}); + + @override + _AdvancedFindFieldState createState() => _AdvancedFindFieldState(); +} + +class _AdvancedFindFieldState extends State<_AdvancedFindField> { + late final controller = TextEditingController(); + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final localeMsg = AppLocalizations.of(context)!; + + return TextField( + controller: controller, + autofocus: false, + style: const TextStyle(fontSize: 14), + decoration: GetFormInputDecoration( + false, + localeMsg.expression, + hint: "name=bladeA&category=device", + iconWidget: Padding( + padding: const EdgeInsets.only(right: 12, left: 12.0), + child: Tooltip( + message: + "${localeMsg.advancedSearchHint} (category=device & name=ibm*) | tag=blade-hp", + verticalOffset: 13, + decoration: const BoxDecoration( + color: Colors.blueAccent, + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + textStyle: const TextStyle( + fontSize: 13, + color: Colors.white, + ), + padding: const EdgeInsets.all(13), + child: const Icon(Icons.info_outline_rounded, + color: Colors.blueAccent), + ), + ), + ), + onSubmitted: (_) => submitted(), + ); + } + + Future submitted() async { + final searchExpression = controller.text.trim(); + final appController = TreeAppController.of(context); + final localeMsg = AppLocalizations.of(context)!; + final messenger = ScaffoldMessenger.of(context); + List nodes; + + var result = await fetchWithComplexFilter( + searchExpression, widget.namespace, localeMsg); + switch (result) { + case Success(value: final foundObjs): + print(foundObjs); + nodes = getTreeNodesFromObjects(foundObjs, appController); + case Failure(exception: final exception): + showSnackBar(messenger, exception.toString(), isError: true); + return; + } + + if (nodes.isEmpty) { + showSnackBar( + messenger, + '${localeMsg.noNodeFound} $searchExpression', + duration: const Duration(seconds: 3), + ); + } else { + showSnackBar( + messenger, + '${localeMsg.xNodesFound(nodes.length)} $searchExpression', + isSuccess: true, + ); + // Expand only until found nodes and scroll to first one + if (!appController.treeController.areAllRootsCollapsed) { + appController.treeController.collapseAll(); + } + for (var node in nodes) { + appController.treeController.expandAncestors(node); + appController.scrollTo(node); + appController.selectNode(node.id); + } + appController.scrollTo(nodes.first); + } + } + + List getTreeNodesFromObjects( + List> foundObjs, TreeAppController appController) { + List nodes = []; + for (var obj in foundObjs) { + var id = obj["id"] as String; + // search for this obj on root node or in its children + for (var root in appController.treeController.roots) { + TreeNode? node; + if (root.id.toLowerCase().contains(id.toLowerCase())) { + node = root; + } else { + node = root.nullableDescendants.firstWhere( + (descendant) => descendant == null + ? false + : descendant.id.toLowerCase().contains(id.toLowerCase()), + orElse: () => null, + ); + } + //found it + if (node != null) { + nodes.add(node); + break; + } + } + } + return nodes; + } +} diff --git a/APP/lib/widgets/select_objects/settings_view/_find_node_field.dart b/APP/lib/widgets/select_objects/settings_view/_find_node_field.dart index 91368dca1..5ef93eb62 100644 --- a/APP/lib/widgets/select_objects/settings_view/_find_node_field.dart +++ b/APP/lib/widgets/select_objects/settings_view/_find_node_field.dart @@ -1,7 +1,7 @@ part of 'settings_view.dart'; class _FindNodeField extends StatefulWidget { - const _FindNodeField({Key? key}) : super(key: key); + const _FindNodeField(); @override __FindNodeFieldState createState() => __FindNodeFieldState(); @@ -18,15 +18,13 @@ class __FindNodeFieldState extends State<_FindNodeField> { @override Widget build(BuildContext context) { - final localeMsg = AppLocalizations.of(context)!; - return TextField( controller: controller, - cursorColor: Colors.blueGrey, autofocus: false, + style: const TextStyle(fontSize: 14), decoration: GetFormInputDecoration( false, - '${localeMsg.search}...', + 'ID', icon: Icons.search_rounded, ), onSubmitted: (_) => _submitted(), @@ -73,6 +71,7 @@ class __FindNodeFieldState extends State<_FindNodeField> { } appController.treeController.expandAncestors(node); appController.scrollTo(node); + appController.selectNode(node.id); } } } diff --git a/APP/lib/widgets/select_objects/settings_view/_header.dart b/APP/lib/widgets/select_objects/settings_view/_header.dart index 7cd0bfa6f..175344a00 100644 --- a/APP/lib/widgets/select_objects/settings_view/_header.dart +++ b/APP/lib/widgets/select_objects/settings_view/_header.dart @@ -22,42 +22,11 @@ class SettingsHeader extends StatelessWidget { child: Text( text, style: GoogleFonts.inter( - fontSize: 17, + fontSize: 15, color: Colors.black, - fontWeight: FontWeight.w500, + fontWeight: FontWeight.w400, ), ), ); } } - -class _ActionsHeader extends StatelessWidget { - const _ActionsHeader({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return const Row( - children: [ - SettingsHeader(text: 'Actions'), - Spacer(), - Tooltip( - message: 'Quick shortcuts', - verticalOffset: 13, - decoration: BoxDecoration( - color: Colors.blueAccent, - borderRadius: BorderRadius.all(Radius.circular(12)), - ), - textStyle: TextStyle( - fontSize: 13, - color: Colors.white, - letterSpacing: 1.025, - height: 1.5, - ), - padding: EdgeInsets.all(16), - child: Icon(Icons.info_outline_rounded, color: Colors.blueAccent), - ), - SizedBox(width: 10), - ], - ); - } -} diff --git a/APP/lib/widgets/select_objects/settings_view/settings_view.dart b/APP/lib/widgets/select_objects/settings_view/settings_view.dart index f9be43e0d..e40815179 100644 --- a/APP/lib/widgets/select_objects/settings_view/settings_view.dart +++ b/APP/lib/widgets/select_objects/settings_view/settings_view.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:ogree_app/common/api_backend.dart'; +import 'package:ogree_app/common/definitions.dart'; import 'package:ogree_app/common/snackbar.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:ogree_app/common/theme.dart'; @@ -12,6 +14,7 @@ part '_actions.dart'; part '_find_node_field.dart'; part '_header.dart'; part '_selected_chips.dart'; +part '_advanced_find_field.dart'; const Duration kAnimationDuration = Duration(milliseconds: 300); @@ -19,10 +22,9 @@ const Color kDarkBlue = Color(0xff1565c0); class SettingsView extends StatelessWidget { final bool isTenantMode; - final bool noFilters; + final Namespace namespace; const SettingsView( - {Key? key, required this.isTenantMode, this.noFilters = false}) - : super(key: key); + {super.key, required this.isTenantMode, required this.namespace}); @override Widget build(BuildContext context) { @@ -52,13 +54,18 @@ class SettingsView extends StatelessWidget { padding: const EdgeInsets.only(left: 16), children: [ const SelectedChips(), - const _ActionsHeader(), + const SettingsHeader(text: 'Actions'), const _Actions(isTenantMode: false), const SizedBox(height: 8), SettingsHeader(text: localeMsg.searchById), const _FindNodeField(), const SizedBox(height: 8), - noFilters ? Container() : const TreeFilter(), + SettingsHeader(text: localeMsg.searchAdvanced), + _AdvancedFindField( + namespace: namespace, + ), + const SizedBox(height: 8), + namespace != Namespace.Physical ? Container() : const TreeFilter(), ], ), ); diff --git a/APP/lib/widgets/select_objects/settings_view/tree_filter.dart b/APP/lib/widgets/select_objects/settings_view/tree_filter.dart index e879d4134..85a67c6cf 100644 --- a/APP/lib/widgets/select_objects/settings_view/tree_filter.dart +++ b/APP/lib/widgets/select_objects/settings_view/tree_filter.dart @@ -156,7 +156,7 @@ class _AutocompleteFilterState extends State { widget.paramLevel == 0 ? Wrap( children: [ - SettingsHeader(text: localeMsg.filters), + SettingsHeader(text: localeMsg.categoryFilters), widget.showClearFilter ? OutlinedButton( style: OutlinedButton.styleFrom( @@ -194,6 +194,7 @@ class _AutocompleteFilterState extends State { return TextFormField( controller: textEditingController, focusNode: focusNode, + style: const TextStyle(fontSize: 14), decoration: GetFormInputDecoration(true, widget.param, isEnabled: widget.enabled), onFieldSubmitted: (String value) { diff --git a/APP/lib/widgets/select_objects/treeapp_controller.dart b/APP/lib/widgets/select_objects/treeapp_controller.dart index 31e270e8a..dccdf1d20 100644 --- a/APP/lib/widgets/select_objects/treeapp_controller.dart +++ b/APP/lib/widgets/select_objects/treeapp_controller.dart @@ -103,13 +103,23 @@ class TreeAppController with ChangeNotifier { void toggleSelection(String id, {bool? shouldSelect, bool shouldNotify = true}) { shouldSelect ??= !isSelected(id); - shouldSelect ? _select(id) : _deselect(id); + shouldSelect ? select(id) : deselect(id); if (shouldNotify) notifyListeners(); } - void _select(String id) => selectedNodes[id] = true; - void _deselect(String id) => selectedNodes.remove(id); + void selectNode(String id) { + select(id); + notifyListeners(); + } + + void deselectNode(String id) { + selectedNodes.remove(id); + notifyListeners(); + } + + void select(String id) => selectedNodes[id] = true; + void deselect(String id) => selectedNodes.remove(id); void selectAll([bool select = true]) { //treeController.expandAll(); diff --git a/APP/lib/widgets/select_objects/view_object_popup.dart b/APP/lib/widgets/select_objects/view_object_popup.dart index 01a0e55f7..db6d995ab 100644 --- a/APP/lib/widgets/select_objects/view_object_popup.dart +++ b/APP/lib/widgets/select_objects/view_object_popup.dart @@ -128,7 +128,9 @@ class _ViewObjectPopupState extends State { var errMsg = ""; // Try both id and slug since we dont know the obj's category for (var keyId in ["id", "slug"]) { - var result = await fetchObject(widget.objId, idKey: keyId); + var result = await fetchObject( + widget.objId, AppLocalizations.of(context)!, + idKey: keyId); switch (result) { case Success(value: final value): if (widget.namespace == Namespace.Logical) { diff --git a/APP/lib/widgets/tenants/domain_view.dart b/APP/lib/widgets/tenants/domain_view.dart index f41cc5fc3..b6149e2d3 100644 --- a/APP/lib/widgets/tenants/domain_view.dart +++ b/APP/lib/widgets/tenants/domain_view.dart @@ -63,7 +63,10 @@ class _DomainViewState extends State { width: 320, height: 116, child: Card( - child: SettingsView(isTenantMode: true))), + child: SettingsView( + isTenantMode: true, + namespace: Namespace.Organisational, + ))), ), ), ]); diff --git a/APP/test/select_objects_test.dart b/APP/test/select_objects_test.dart index 024ebf511..e8890b50a 100644 --- a/APP/test/select_objects_test.dart +++ b/APP/test/select_objects_test.dart @@ -71,8 +71,8 @@ void main() { ))); const searchStr = "rack2.devB.devB-2"; - final searchInput = find.ancestor( - of: find.text('Rechercher...'), matching: find.byType(TextField)); + final searchInput = + find.ancestor(of: find.text('ID'), matching: find.byType(TextField)); await tester.enterText(searchInput, searchStr); await tester.testTextInput.receiveAction(TextInputAction.done); await tester.pumpAndSettle();