diff --git a/lib/ui/app/entity_dropdown.dart b/lib/ui/app/entity_dropdown.dart index 10b89fe2f..b22ad1e30 100644 --- a/lib/ui/app/entity_dropdown.dart +++ b/lib/ui/app/entity_dropdown.dart @@ -302,7 +302,7 @@ class _EntityDropdownState extends State { ? kDefaultDarkSelectedColor : kDefaultLightSelectedColor) : Theme.of(context).cardColor, - child: _EntityListTile( + child: EntityAutocompleteListTile( onTap: (entity) => onSelected(entity), entity: options.elementAt(index), filter: _filter, @@ -456,7 +456,7 @@ class _EntityDropdownDialogState extends State { itemBuilder: (BuildContext context, int index) { final entityId = matches[index]; final entity = widget.entityMap[entityId]; - return _EntityListTile( + return EntityAutocompleteListTile( entity: entity, filter: _filter, onTap: (entity) => _selectEntity(entity), @@ -482,12 +482,12 @@ class _EntityDropdownDialogState extends State { } } -class _EntityListTile extends StatelessWidget { - const _EntityListTile({ +class EntityAutocompleteListTile extends StatelessWidget { + const EntityAutocompleteListTile({ @required this.entity, - @required this.filter, - @required this.overrideSuggestedLabel, - @required this.overrideSuggestedAmount, + this.filter, + this.overrideSuggestedLabel, + this.overrideSuggestedAmount, this.onTap, }); diff --git a/lib/ui/invoice/edit/invoice_edit_items_desktop.dart b/lib/ui/invoice/edit/invoice_edit_items_desktop.dart index ba2a31ed9..2a631b755 100644 --- a/lib/ui/invoice/edit/invoice_edit_items_desktop.dart +++ b/lib/ui/invoice/edit/invoice_edit_items_desktop.dart @@ -4,7 +4,10 @@ import 'package:flutter/material.dart'; // Package imports: import 'package:flutter_redux/flutter_redux.dart'; -import 'package:flutter_typeahead/flutter_typeahead.dart'; +import 'package:invoiceninja_flutter/data/models/product_model.dart'; +import 'package:invoiceninja_flutter/ui/app/app_border.dart'; +import 'package:invoiceninja_flutter/ui/app/entity_dropdown.dart'; +import 'package:invoiceninja_flutter/ui/app/scrollable_listview.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; // Project imports: @@ -45,6 +48,7 @@ class InvoiceEditItemsDesktop extends StatefulWidget { class _InvoiceEditItemsDesktopState extends State { final _debouncer = Debouncer(); + TextEditingController _textEditingController; bool _isReordering = false; int _updatedAt; int _autocompleteFocusIndex = -1; @@ -94,6 +98,7 @@ class _InvoiceEditItemsDesktopState extends State { @override Widget build(BuildContext context) { final localization = AppLocalization.of(context); + final theme = Theme.of(context); final viewModel = widget.viewModel; final state = viewModel.state; final company = state.company; @@ -470,277 +475,153 @@ class _InvoiceEditItemsDesktopState extends State { key: ValueKey( '__line_item_${index}_${lineItems[index].createdAt}__'), children: [ - /* - Padding( - padding: const EdgeInsets.only(right: kTableColumnGap), - child: Autocomplete( - //key: ValueKey('__line_item_${index}_name__'), - optionsBuilder: (TextEditingValue textEditingValue) { - final options = productIds - .map((productId) => productState.map[productId]) - .where((product) => product.productKey - .toLowerCase() - .contains(_filter.toLowerCase())) - .toList(); -/* - if (options.length == 1 && - options[0].productKey.toLowerCase() == - _filter.toLowerCase()) { - return []; - } - */ - - return options; - }, - displayStringForOption: (product) => product.productKey, - onSelected: (product) { - final item = lineItems[index]; - final client = - state.clientState.get(invoice.clientId); - final currency = - state.staticState.currencyMap[client.currencyId]; - - double cost = product.price; - if (company.convertProductExchangeRate && - invoice.clientId != null && - client.currencyId != company.currencyId) { - cost = round(cost * invoice.exchangeRate, - currency.precision); - } - - final updatedItem = item.rebuild((b) => b - ..productKey = product.productKey - ..notes = item.isTask ? item.notes : product.notes - ..cost = - item.isTask && item.cost != 0 ? item.cost : cost - ..quantity = item.isTask || item.quantity != 0 - ? item.quantity - : viewModel.state.company.defaultQuantity - ? 1 - : product.quantity - ..customValue1 = product.customValue1 - ..customValue2 = product.customValue2 - ..customValue3 = product.customValue3 - ..customValue4 = product.customValue4 - ..taxName1 = company.numberOfItemTaxRates >= 1 ? product.taxName1 : '' - ..taxRate1 = company.numberOfItemTaxRates >= 1 ? product.taxRate1 : 0 - ..taxName2 = company.numberOfItemTaxRates >= 2 ? product.taxName2 : '' - ..taxRate2 = company.numberOfItemTaxRates >= 2 ? product.taxRate2 : 0 - ..taxName3 = company.numberOfItemTaxRates >= 3 ? product.taxName3 : '' - ..taxRate3 = company.numberOfItemTaxRates >= 3 ? product.taxRate3 : 0); - _onChanged(updatedItem, index); - _updateTable(); - }, - fieldViewBuilder: (BuildContext context, - TextEditingController textEditingController, - FocusNode focusNode, - VoidCallback onFieldSubmitted) { - return DecoratedFormField( - showClear: false, - autofocus: false, - controller: textEditingController, - focusNode: focusNode, - onFieldSubmitted: (String value) { - onFieldSubmitted(); - }, - onChanged: (value) { - _filter = value; - _onChanged( - lineItems[index] - .rebuild((b) => b..productKey = value), - index); - }, - ); - }, - ), - ), - */ Focus( onFocusChange: (hasFocus) { + print('## onFocusChange - hasFocus: $hasFocus'); _autocompleteFocusIndex = index; _onFocusChange(); }, skipTraversal: true, child: Padding( - padding: - const EdgeInsets.only(right: kTableColumnGap), - child: TypeAheadFormField( - key: ValueKey('__line_item_${index}_name__'), - initialValue: lineItems[index].productKey, - noItemsFoundBuilder: (context) => SizedBox(), - suggestionsCallback: (pattern) { - return productIds - .where((productId) => productState - .map[productId].productKey - .toLowerCase() - .contains(pattern.toLowerCase())) - .toList(); - /* - return productIds - .where((productId) => productState.map[productId] - .matchesFilter(pattern)) - .toList(); - */ - }, - itemBuilder: (context, productId) { - // TODO fix this - /* - return ListTile( - title: Text(productState.map[suggestion].productKey), - ); - */ - return Listener( - child: Container( - color: Theme.of(context).cardColor, - child: ListTile( - title: Text( - productState.map[productId].productKey), + padding: const EdgeInsets.only(right: kTableColumnGap), + child: RawAutocomplete( + key: ValueKey('__line_item_${index}_name__'), + textEditingController: _textEditingController, + initialValue: TextEditingValue( + text: lineItems[index].productKey), + optionsBuilder: (TextEditingValue textEditingValue) { + print( + '## optionsBuilder - filter: ${textEditingValue.text}'); + final options = productIds + .map((productId) => productState.map[productId]) + .where((product) => product.productKey + .toLowerCase() + .contains( + textEditingValue.text.toLowerCase())) + .toList(); + + return options; + }, + displayStringForOption: (product) => + product.productKey, + onSelected: (product) { + final item = lineItems[index]; + final client = + state.clientState.get(invoice.clientId); + final currency = state + .staticState.currencyMap[client.currencyId]; + + double cost = product.price; + if (company.convertProductExchangeRate && + invoice.clientId != null && + client.currencyId != company.currencyId) { + cost = round(cost * invoice.exchangeRate, + currency.precision); + } + + final updatedItem = item.rebuild((b) => b + ..productKey = product.productKey + ..notes = item.isTask ? item.notes : product.notes + ..productCost = item.productCost + ..cost = item.isTask && item.cost != 0 + ? item.cost + : cost + ..quantity = item.isTask || item.quantity != 0 + ? item.quantity + : viewModel.state.company.defaultQuantity + ? 1 + : product.quantity + ..customValue1 = product.customValue1 + ..customValue2 = product.customValue2 + ..customValue3 = product.customValue3 + ..customValue4 = product.customValue4 + ..taxName1 = company.numberOfItemTaxRates >= 1 + ? product.taxName1 + : '' + ..taxRate1 = company.numberOfItemTaxRates >= 1 + ? product.taxRate1 + : 0 + ..taxName2 = company.numberOfItemTaxRates >= 2 + ? product.taxName2 + : '' + ..taxRate2 = company.numberOfItemTaxRates >= 2 + ? product.taxRate2 + : 0 + ..taxName3 = company.numberOfItemTaxRates >= 3 + ? product.taxName3 + : '' + ..taxRate3 = company.numberOfItemTaxRates >= 3 + ? product.taxRate3 + : 0); + _onChanged(updatedItem, index, debounce: false); + _updateTable(); + }, + fieldViewBuilder: (BuildContext context, + TextEditingController textEditingController, + FocusNode focusNode, + VoidCallback onFieldSubmitted) { + return DecoratedFormField( + showClear: false, + autofocus: false, + controller: textEditingController, + focusNode: focusNode, + onFieldSubmitted: (String value) { + onFieldSubmitted(); + }, + onChanged: (value) { + print('## Set filter to $value'); + _onChanged( + lineItems[index] + .rebuild((b) => b..productKey = value), + index); + }, + ); + }, + optionsViewBuilder: (BuildContext context, + AutocompleteOnSelected onSelected, + Iterable options) { + final highlightedIndex = + AutocompleteHighlightedOption.of(context); + return Theme( + data: theme, + child: Align( + alignment: Alignment.topLeft, + child: Material( + elevation: 4, + child: AppBorder( + child: Container( + color: Theme.of(context).cardColor, + width: 250, + constraints: + BoxConstraints(maxHeight: 270), + child: ScrollableListViewBuilder( + itemCount: options.length, + itemBuilder: + (BuildContext context, int index) { + return Container( + color: highlightedIndex == index + ? convertHexStringToColor(state + .prefState + .enableDarkMode + ? kDefaultDarkSelectedColor + : kDefaultLightSelectedColor) + : Theme.of(context).cardColor, + child: EntityAutocompleteListTile( + onTap: (entity) => + onSelected(entity), + entity: options.elementAt(index), + ), + ); + }, + ), + ), ), ), - onPointerDown: (_) { - if (!kIsWeb) { - return; - } - - final item = lineItems[index]; - final product = productState.map[productId]; - final client = - state.clientState.get(invoice.clientId); - final currency = state.staticState - .currencyMap[client.currencyId]; - - double cost = product.price; - if (company.convertProductExchangeRate && - invoice.clientId != null && - client.currencyId != company.currencyId) { - cost = round(cost * invoice.exchangeRate, - currency.precision); - } - - final updatedItem = item.rebuild((b) => b - ..productKey = product.productKey - ..notes = - item.isTask || !company.fillProducts - ? item.notes - : product.notes - ..cost = (item.isTask && item.cost != 0) || - !company.fillProducts - ? item.cost - : cost - ..productCost = product.cost - ..quantity = - item.isTask || item.quantity != 0 - ? item.quantity - : product.quantity == 0 - ? 1 - : product.quantity - ..customValue1 = product.customValue1 - ..customValue2 = product.customValue2 - ..customValue3 = product.customValue3 - ..customValue4 = product.customValue4 - ..taxName1 = - company.numberOfItemTaxRates >= 1 - ? product.taxName1 - : '' - ..taxRate1 = - company.numberOfItemTaxRates >= 1 - ? product.taxRate1 - : 0 - ..taxName2 = - company.numberOfItemTaxRates >= 2 - ? product.taxName2 - : '' - ..taxRate2 = - company.numberOfItemTaxRates >= 2 - ? product.taxRate2 - : 0 - ..taxName3 = - company.numberOfItemTaxRates >= 3 - ? product.taxName3 - : '' - ..taxRate3 = - company.numberOfItemTaxRates >= 3 - ? product.taxRate3 - : 0); - _onChanged(updatedItem, index, - debounce: false); - _updateTable(); - }, - ); - }, - onSuggestionSelected: (suggestion) { - if (kIsWeb) { - return; - } - - final item = lineItems[index]; - final product = productState.map[suggestion]; - final client = - state.clientState.get(invoice.clientId); - - double cost = product.price; - if (company.convertProductExchangeRate && - invoice.clientId != null && - client.currencyId != company.currencyId) { - cost = round( - cost * invoice.exchangeRate, - state - .staticState - .currencyMap[client?.currencyId ?? - company.currencyId] - .precision); - } - final updatedItem = item.rebuild((b) => b - ..productKey = product.productKey - ..notes = item.isTask || !company.fillProducts - ? item.notes - : product.notes - ..cost = (item.isTask && item.cost != 0) || - !company.fillProducts - ? item.cost - : cost - ..productCost = product.cost - ..quantity = item.isTask || item.quantity != 0 - ? item.quantity - : product.quantity == 0 - ? 1 - : product.quantity - ..customValue1 = product.customValue1 - ..customValue2 = product.customValue2 - ..customValue3 = product.customValue3 - ..customValue4 = product.customValue4 - ..taxName1 = company.numberOfItemTaxRates >= 1 - ? product.taxName1 - : '' - ..taxRate1 = company.numberOfItemTaxRates >= 1 - ? product.taxRate1 - : 0 - ..taxName2 = company.numberOfItemTaxRates >= 2 - ? product.taxName2 - : '' - ..taxRate2 = company.numberOfItemTaxRates >= 2 - ? product.taxRate2 - : 0 - ..taxName3 = company.numberOfItemTaxRates >= 3 - ? product.taxName3 - : '' - ..taxRate3 = company.numberOfItemTaxRates >= 3 - ? product.taxRate3 - : 0); - _onChanged(updatedItem, index, debounce: false); - _updateTable(); - }, - textFieldConfiguration: - TextFieldConfiguration(onChanged: (value) { - _onChanged( - lineItems[index] - .rebuild((b) => b..productKey = value), - index); - }), - autoFlipDirection: true, - animationStart: 1, - debounceDuration: Duration(seconds: 0), - )), + ), + ); + }, + ), + ), ), Focus( onFocusChange: (hasFocus) => _onFocusChange(),