From efb7ff4170a0a3915746357cb0fccc7b08756d85 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 10 Apr 2023 17:07:06 +1000 Subject: [PATCH] Keyboard fix (#306) * Remove focusNode in search widget - Was causing some issues in iOS in particular * Improve search UX by cancelling previous query - Prevent successive search queries from being displayed * Update release notes * Add "type" for cancelable operation --- assets/release_notes.md | 7 +++ lib/widget/search.dart | 94 +++++++++++++++++++---------------------- 2 files changed, 51 insertions(+), 50 deletions(-) diff --git a/assets/release_notes.md b/assets/release_notes.md index 6a858f7e..5b638d9c 100644 --- a/assets/release_notes.md +++ b/assets/release_notes.md @@ -1,6 +1,13 @@ ## InvenTree App Release Notes --- +### 0.11.1 - April 2023 +--- + +- Fixes keyboard bug in search widget +- Adds ability to create new purchase orders directly from the app +- Adds support for the "contact" field to purchase orders + ### 0.11.0 - April 2023 --- diff --git a/lib/widget/search.dart b/lib/widget/search.dart index 2f7ddb30..07860db0 100644 --- a/lib/widget/search.dart +++ b/lib/widget/search.dart @@ -1,5 +1,5 @@ import "dart:async"; - +import "package:async/async.dart"; import "package:flutter/material.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; @@ -40,16 +40,10 @@ class _SearchDisplayState extends RefreshableState { final bool hasAppBar; - @override - void initState() { - super.initState(); - - _focusNode = FocusNode(); - } + CancelableOperation? _search_query; @override void dispose() { - _focusNode.dispose(); super.dispose(); } @@ -99,8 +93,6 @@ class _SearchDisplayState extends RefreshableState { int nSupplierResults = 0; int nPurchaseOrderResults = 0; - late FocusNode _focusNode; - // Callback when the text is being edited // Incorporates a debounce timer to restrict search frequency void onSearchTextChanged(String text, {bool immediate = false}) { @@ -114,9 +106,6 @@ class _SearchDisplayState extends RefreshableState { } else { debounceTimer = Timer(Duration(milliseconds: 250), () { search(text); - if (!_focusNode.hasFocus) { - _focusNode.requestFocus(); - } }); } } @@ -147,12 +136,36 @@ class _SearchDisplayState extends RefreshableState { return count; } + // Actually perform the search query + Future _perform_search(Map body) async { + InvenTreeAPI().post( + "search/", + body: body, + expectedStatusCode: 200).then((APIResponse response) { + decrementPendingSearches(); + + + Map results = {}; + + if (response.data is Map) { + results = response.data as Map; + } + + if (mounted) { + setState(() { + nPartResults = getSearchResultCount(results, "part"); + nCategoryResults = getSearchResultCount(results, "partcategory"); + nStockResults = getSearchResultCount(results, "stockitem"); + nLocationResults = getSearchResultCount(results, "stocklocation"); + nSupplierResults = 0; //getSearchResultCount(results, "") + nPurchaseOrderResults = getSearchResultCount(results, "purchaseorder"); + }); + } + }); + } + /* - * Initiate multiple search requests to the server. - * Each request returns at *some point* in the future, - * by which time the search input may have changed, giving unexpected results. - * - * So, each request only causes an update *if* the search term is still the same when it completes + * Callback when the search input is changed */ Future search(String term) async { var api = InvenTreeAPI(); @@ -173,6 +186,15 @@ class _SearchDisplayState extends RefreshableState { nPendingSearches = 0; }); + // Cancel the previous search query (if in progress) + if (_search_query != null) { + if (!_search_query!.isCanceled) { + _search_query!.cancel(); + } + } + + _search_query = null; + if (term.isEmpty) { return; } @@ -216,29 +238,9 @@ class _SearchDisplayState extends RefreshableState { if (body.isNotEmpty) { nPendingSearches++; - api.post( - "search/", - body: body, - expectedStatusCode: 200).then((APIResponse response) { - decrementPendingSearches(); - - Map results = {}; - - if (response.data is Map) { - results = response.data as Map; - } - - if (mounted) { - setState(() { - nPartResults = getSearchResultCount(results, "part"); - nCategoryResults = getSearchResultCount(results, "partcategory"); - nStockResults = getSearchResultCount(results, "stockitem"); - nLocationResults = getSearchResultCount(results, "stocklocation"); - nSupplierResults = 0; //getSearchResultCount(results, "") - nPurchaseOrderResults = getSearchResultCount(results, "purchaseorder"); - }); - } - }); + _search_query = CancelableOperation.fromFuture( + _perform_search(body), + ); } } else { legacySearch(term); @@ -344,16 +346,13 @@ class _SearchDisplayState extends RefreshableState { ), key: _formKey, readOnly: false, - autofocus: false, + autofocus: true, autocorrect: false, - focusNode: _focusNode, controller: searchController, onChanged: (String text) { onSearchTextChanged(text); - _focusNode.requestFocus(); }, onFieldSubmitted: (String text) { - _focusNode.requestFocus(); }, ), trailing: GestureDetector( @@ -363,7 +362,6 @@ class _SearchDisplayState extends RefreshableState { ), onTap: () { searchController.clear(); - _focusNode.requestFocus(); onSearchTextChanged("", immediate: true); }, ), @@ -541,10 +539,6 @@ class _SearchDisplayState extends RefreshableState { } } - if (!_focusNode.hasFocus) { - _focusNode.requestFocus(); - } - return tiles; }