From c389866edbfb00270b98875d1e9ac2a3c6de0596 Mon Sep 17 00:00:00 2001 From: Gianfranco Gasbarri Date: Sat, 19 Oct 2019 22:36:15 +0100 Subject: [PATCH] Implemented Multiselect for Expense --- lib/main.dart | 29 ++--- lib/redux/expense/expense_actions.dart | 71 +++++++++-- lib/redux/expense/expense_reducer.dart | 27 +++++ .../company_gateway_list_item.dart | 6 + lib/ui/document/document_list_item.dart | 6 + lib/ui/expense/expense_list.dart | 19 ++- lib/ui/expense/expense_list_item.dart | 110 +++++++++++++----- lib/ui/expense/expense_list_vm.dart | 2 +- lib/ui/expense/expense_screen.dart | 80 +++++++++++-- lib/ui/expense/expense_screen_vm.dart | 64 ++++++++++ lib/ui/expense/view/expense_view_vm.dart | 2 +- 11 files changed, 349 insertions(+), 67 deletions(-) create mode 100644 lib/ui/expense/expense_screen_vm.dart diff --git a/lib/main.dart b/lib/main.dart index aea284d00..7b3e499cb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,6 +13,7 @@ import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/redux/auth/auth_middleware.dart'; import 'package:invoiceninja_flutter/redux/client/client_middleware.dart'; import 'package:invoiceninja_flutter/redux/company/company_selectors.dart'; +import 'package:invoiceninja_flutter/redux/company_gateway/company_gateway_middleware.dart'; import 'package:invoiceninja_flutter/redux/dashboard/dashboard_middleware.dart'; import 'package:invoiceninja_flutter/redux/document/document_middleware.dart'; import 'package:invoiceninja_flutter/redux/expense/expense_middleware.dart'; @@ -24,6 +25,7 @@ import 'package:invoiceninja_flutter/redux/project/project_middleware.dart'; import 'package:invoiceninja_flutter/redux/quote/quote_middleware.dart'; import 'package:invoiceninja_flutter/redux/settings/settings_middleware.dart'; import 'package:invoiceninja_flutter/redux/task/task_middleware.dart'; +import 'package:invoiceninja_flutter/redux/tax_rate/tax_rate_middleware.dart'; import 'package:invoiceninja_flutter/redux/ui/ui_state.dart'; import 'package:invoiceninja_flutter/redux/vendor/vendor_middleware.dart'; import 'package:invoiceninja_flutter/ui/app/app_builder.dart'; @@ -31,7 +33,13 @@ import 'package:invoiceninja_flutter/ui/app/main_screen.dart'; import 'package:invoiceninja_flutter/ui/app/screen_imports.dart'; import 'package:invoiceninja_flutter/ui/auth/init_screen.dart'; import 'package:invoiceninja_flutter/ui/auth/login_vm.dart'; +import 'package:invoiceninja_flutter/ui/company_gateway/company_gateway_screen.dart'; +import 'package:invoiceninja_flutter/ui/company_gateway/company_gateway_screen_vm.dart'; +import 'package:invoiceninja_flutter/ui/company_gateway/edit/company_gateway_edit_vm.dart'; +import 'package:invoiceninja_flutter/ui/company_gateway/view/company_gateway_view_vm.dart'; import 'package:invoiceninja_flutter/ui/dashboard/dashboard_vm.dart'; +import 'package:invoiceninja_flutter/ui/document/document_screen_vm.dart'; +import 'package:invoiceninja_flutter/ui/expense/expense_screen_vm.dart'; import 'package:invoiceninja_flutter/ui/group/edit/group_edit_vm.dart'; import 'package:invoiceninja_flutter/ui/group/group_screen.dart'; import 'package:invoiceninja_flutter/ui/group/view/group_view_vm.dart'; @@ -53,6 +61,9 @@ import 'package:invoiceninja_flutter/ui/settings/products_vm.dart'; import 'package:invoiceninja_flutter/ui/settings/tax_rates_vm.dart'; import 'package:invoiceninja_flutter/ui/settings/templates_and_reminders_vm.dart'; import 'package:invoiceninja_flutter/ui/settings/user_details_vm.dart'; +import 'package:invoiceninja_flutter/ui/tax_rate/edit/tax_rate_edit_vm.dart'; +import 'package:invoiceninja_flutter/ui/tax_rate/tax_rate_screen.dart'; +import 'package:invoiceninja_flutter/ui/tax_rate/view/tax_rate_view_vm.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:local_auth/local_auth.dart'; import 'package:redux/redux.dart'; @@ -60,17 +71,6 @@ import 'package:redux_logging/redux_logging.dart'; import 'package:sentry/sentry.dart'; import 'package:shared_preferences/shared_preferences.dart'; -// STARTER: import - do not remove comment -import 'package:invoiceninja_flutter/ui/tax_rate/tax_rate_screen.dart'; -import 'package:invoiceninja_flutter/ui/tax_rate/edit/tax_rate_edit_vm.dart'; -import 'package:invoiceninja_flutter/ui/tax_rate/view/tax_rate_view_vm.dart'; -import 'package:invoiceninja_flutter/redux/tax_rate/tax_rate_middleware.dart'; - -import 'package:invoiceninja_flutter/ui/company_gateway/company_gateway_screen.dart'; -import 'package:invoiceninja_flutter/ui/company_gateway/edit/company_gateway_edit_vm.dart'; -import 'package:invoiceninja_flutter/ui/company_gateway/view/company_gateway_view_vm.dart'; -import 'package:invoiceninja_flutter/redux/company_gateway/company_gateway_middleware.dart'; - void main({bool isTesting = false}) async { final SentryClient _sentry = Config.SENTRY_DNS.isEmpty ? null @@ -306,10 +306,10 @@ class InvoiceNinjaAppState extends State { InvoiceViewScreen.route: (context) => InvoiceViewScreen(), InvoiceEditScreen.route: (context) => InvoiceEditScreen(), InvoiceEmailScreen.route: (context) => InvoiceEmailScreen(), - DocumentScreen.route: (context) => DocumentScreen(), + DocumentScreen.route: (context) => DocumentScreenBuilder(), DocumentViewScreen.route: (context) => DocumentViewScreen(), DocumentEditScreen.route: (context) => DocumentEditScreen(), - ExpenseScreen.route: (context) => ExpenseScreen(), + ExpenseScreen.route: (context) => ExpenseScreenBuilder(), ExpenseViewScreen.route: (context) => ExpenseViewScreen(), ExpenseEditScreen.route: (context) => ExpenseEditScreen(), VendorScreen.route: (context) => VendorScreen(), @@ -336,7 +336,8 @@ class InvoiceNinjaAppState extends State { CompanyDetailsScreen.route: (context) => CompanyDetailsScreen(), UserDetailsScreen.route: (context) => UserDetailsScreen(), LocalizationScreen.route: (context) => LocalizationScreen(), - CompanyGatewayScreen.route: (context) => CompanyGatewayScreen(), + CompanyGatewayScreen.route: (context) => + CompanyGatewayScreenBuilder(), CompanyGatewayViewScreen.route: (context) => CompanyGatewayViewScreen(), CompanyGatewayEditScreen.route: (context) => diff --git a/lib/redux/expense/expense_actions.dart b/lib/redux/expense/expense_actions.dart index b030ff9d5..28e00bb71 100644 --- a/lib/redux/expense/expense_actions.dart +++ b/lib/redux/expense/expense_actions.dart @@ -1,15 +1,16 @@ import 'dart:async'; -import 'package:flutter/widgets.dart'; + import 'package:built_collection/built_collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_redux/flutter_redux.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; -import 'package:flutter_redux/flutter_redux.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; -import 'package:invoiceninja_flutter/redux/invoice/invoice_actions.dart'; -import 'package:invoiceninja_flutter/utils/localization.dart'; -import 'package:invoiceninja_flutter/utils/completers.dart'; -import 'package:flutter/material.dart'; import 'package:invoiceninja_flutter/redux/expense/expense_selectors.dart'; +import 'package:invoiceninja_flutter/redux/invoice/invoice_actions.dart'; +import 'package:invoiceninja_flutter/utils/completers.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; class ViewExpenseList implements PersistUI { ViewExpenseList({@required this.context, this.force = false}); @@ -245,11 +246,22 @@ class FilterExpensesByEntity implements PersistUI { } void handleExpenseAction( - BuildContext context, ExpenseEntity expense, EntityAction action) { + BuildContext context, List expenses, EntityAction action) { + assert( + [ + EntityAction.restore, + EntityAction.archive, + EntityAction.delete, + EntityAction.toggleMultiselect + ].contains(action) || + expenses.length == 1, + 'Cannot perform this action on more than one expense'); + final store = StoreProvider.of(context); final state = store.state; final CompanyEntity company = state.selectedCompany; final localization = AppLocalization.of(context); + final expense = expenses.first; switch (action) { case EntityAction.edit: @@ -286,5 +298,50 @@ void handleExpenseAction( store.dispatch(DeleteExpenseRequest( snackBarCompleter(context, localization.deletedExpense), expense.id)); break; + case EntityAction.toggleMultiselect: + if (!store.state.expenseListState.isInMultiselect()) { + store.dispatch(StartExpenseMultiselect(context: context)); + } + + if (expenses.isEmpty) { + break; + } + + for (final expense in expenses) { + if (!store.state.expenseListState.isSelected(expense)) { + store.dispatch( + AddToExpenseMultiselect(context: context, entity: expense)); + } else { + store.dispatch( + RemoveFromExpenseMultiselect(context: context, entity: expense)); + } + } + break; } } + +class StartExpenseMultiselect { + StartExpenseMultiselect({@required this.context}); + + final BuildContext context; +} + +class AddToExpenseMultiselect { + AddToExpenseMultiselect({@required this.context, @required this.entity}); + + final BuildContext context; + final BaseEntity entity; +} + +class RemoveFromExpenseMultiselect { + RemoveFromExpenseMultiselect({@required this.context, @required this.entity}); + + final BuildContext context; + final BaseEntity entity; +} + +class ClearExpenseMultiselect { + ClearExpenseMultiselect({@required this.context}); + + final BuildContext context; +} diff --git a/lib/redux/expense/expense_reducer.dart b/lib/redux/expense/expense_reducer.dart index 3cf28b7a3..214811157 100644 --- a/lib/redux/expense/expense_reducer.dart +++ b/lib/redux/expense/expense_reducer.dart @@ -52,6 +52,11 @@ final expenseListReducer = combineReducers([ TypedReducer(_filterExpensesByCustom1), TypedReducer(_filterExpensesByCustom2), TypedReducer(_filterExpensesByClient), + TypedReducer(_startListMultiselect), + TypedReducer(_addToListMultiselect), + TypedReducer( + _removeFromListMultiselect), + TypedReducer(_clearListMultiselect), ]); ListUIState _filterExpensesByClient( @@ -116,6 +121,28 @@ ListUIState _sortExpenses(ListUIState expenseListState, SortExpenses action) { ..sortField = action.field); } +ListUIState _startListMultiselect( + ListUIState expenseListState, StartExpenseMultiselect action) { + return expenseListState.rebuild((b) => b..selectedEntities = []); +} + +ListUIState _addToListMultiselect( + ListUIState expenseListState, AddToExpenseMultiselect action) { + return expenseListState + .rebuild((b) => b..selectedEntities.add(action.entity)); +} + +ListUIState _removeFromListMultiselect( + ListUIState expenseListState, RemoveFromExpenseMultiselect action) { + return expenseListState + .rebuild((b) => b..selectedEntities.remove(action.entity)); +} + +ListUIState _clearListMultiselect( + ListUIState expenseListState, ClearExpenseMultiselect action) { + return expenseListState.rebuild((b) => b..selectedEntities = null); +} + final expensesReducer = combineReducers([ TypedReducer(_updateExpense), TypedReducer(_addExpense), diff --git a/lib/ui/company_gateway/company_gateway_list_item.dart b/lib/ui/company_gateway/company_gateway_list_item.dart index 468c6c5e9..26fdc26c0 100644 --- a/lib/ui/company_gateway/company_gateway_list_item.dart +++ b/lib/ui/company_gateway/company_gateway_list_item.dart @@ -53,6 +53,12 @@ class _CompanyGatewayListItemState extends State final isInMultiselect = listUIState.isInMultiselect(); final showCheckbox = widget.onCheckboxChanged != null || isInMultiselect; + if (isInMultiselect) { + _multiselectCheckboxAnimController.forward(); + } else { + _multiselectCheckboxAnimController.animateBack(0.0); + } + return DismissibleEntity( userCompany: state.userCompany, entity: widget.companyGateway, diff --git a/lib/ui/document/document_list_item.dart b/lib/ui/document/document_list_item.dart index 9f5cfb126..f9b7c29ab 100644 --- a/lib/ui/document/document_list_item.dart +++ b/lib/ui/document/document_list_item.dart @@ -52,6 +52,12 @@ class _DocumentListItemState extends State final isInMultiselect = listUIState.isInMultiselect(); final showCheckbox = widget.onCheckboxChanged != null || isInMultiselect; + if (isInMultiselect) { + _multiselectCheckboxAnimController.forward(); + } else { + _multiselectCheckboxAnimController.animateBack(0.0); + } + return DismissibleEntity( isSelected: widget.document.id == (uiState.isEditing diff --git a/lib/ui/expense/expense_list.dart b/lib/ui/expense/expense_list.dart index ab9667e51..5605acec1 100644 --- a/lib/ui/expense/expense_list.dart +++ b/lib/ui/expense/expense_list.dart @@ -1,6 +1,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/redux/document/document_selectors.dart'; import 'package:invoiceninja_flutter/ui/app/entities/entity_actions_dialog.dart'; import 'package:invoiceninja_flutter/ui/app/help_text.dart'; @@ -44,6 +46,9 @@ class ExpenseList extends StatelessWidget { onClearPressed: viewModel.onClearEntityFilterPressed, )); } + final store = StoreProvider.of(context); + final listUIState = store.state.uiState.expenseUIState.listUIState; + final isInMultiselect = listUIState.isInMultiselect(); widgets.add(Expanded( child: !viewModel.isLoaded @@ -87,7 +92,19 @@ class ExpenseList extends StatelessWidget { context, [expense], action); } }, - onLongPress: () => showDialog(), + onLongPress: () async { + final longPressIsSelection = store.state.uiState + .longPressSelectionIsDefault ?? + true; + if (longPressIsSelection && !isInMultiselect) { + viewModel.onEntityAction(context, [expense], + EntityAction.toggleMultiselect); + } else { + showDialog(); + } + }, + isChecked: isInMultiselect && + listUIState.isSelected(expense), ); }, ), diff --git a/lib/ui/expense/expense_list_item.dart b/lib/ui/expense/expense_list_item.dart index d24df5082..e243cb5c4 100644 --- a/lib/ui/expense/expense_list_item.dart +++ b/lib/ui/expense/expense_list_item.dart @@ -9,7 +9,7 @@ import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/ui/app/dismissible_entity.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; -class ExpenseListItem extends StatelessWidget { +class ExpenseListItem extends StatefulWidget { const ExpenseListItem({ @required this.userCompany, @required this.onTap, @@ -38,6 +38,12 @@ class ExpenseListItem extends StatelessWidget { static final expenseItemKey = (int id) => Key('__expense_item_${id}__'); + @override + _ExpenseListItemState createState() => _ExpenseListItemState(); +} + +class _ExpenseListItemState extends State + with TickerProviderStateMixin { @override Widget build(BuildContext context) { final localization = AppLocalization.of(context); @@ -45,56 +51,77 @@ class ExpenseListItem extends StatelessWidget { final uiState = state.uiState; final expenseUIState = uiState.expenseUIState; - final filterMatch = filter != null && filter.isNotEmpty - ? expense.matchesFilterValue(filter) + final filterMatch = widget.filter != null && widget.filter.isNotEmpty + ? widget.expense.matchesFilterValue(widget.filter) : null; final company = state.selectedCompany; - final category = company.expenseCategoryMap[expense.categoryId]; + final category = company.expenseCategoryMap[widget.expense.categoryId]; String subtitle = ''; if (filterMatch != null) { subtitle = filterMatch; - } else if (client != null || vendor != null || category != null) { + } else if (widget.client != null || + widget.vendor != null || + category != null) { if (category != null) { subtitle += category.name; - if (vendor != null || client != null) { + if (widget.vendor != null || widget.client != null) { subtitle += ' • '; } } - if (vendor != null) { - subtitle += vendor.name; - if (client != null) { + if (widget.vendor != null) { + subtitle += widget.vendor.name; + if (widget.client != null) { subtitle += ' • '; } } - if (client != null) { - subtitle += client.displayName; + if (widget.client != null) { + subtitle += widget.client.displayName; } } - if (hasDocuments) { + if (widget.hasDocuments) { if (subtitle.isNotEmpty) { subtitle += ' '; } subtitle += '📎'; } + final listUIState = expenseUIState.listUIState; + final isInMultiselect = listUIState.isInMultiselect(); + final showCheckbox = widget.onCheckboxChanged != null || isInMultiselect; + + if (isInMultiselect) { + _multiselectCheckboxAnimController.forward(); + } else { + _multiselectCheckboxAnimController.animateBack(0.0); + } return DismissibleEntity( - isSelected: expense.id == + isSelected: widget.expense.id == (uiState.isEditing ? expenseUIState.editing.id : expenseUIState.selectedId), - userCompany: userCompany, - entity: expense, - onEntityAction: onEntityAction, + userCompany: widget.userCompany, + entity: widget.expense, + onEntityAction: widget.onEntityAction, child: ListTile( - onTap: onTap, - onLongPress: onLongPress, - leading: onCheckboxChanged != null - ? Checkbox( - value: isChecked, - onChanged: (value) => onCheckboxChanged(value), - activeColor: Theme.of(context).accentColor, + onTap: isInMultiselect + ? () => widget.onEntityAction(EntityAction.toggleMultiselect) + : widget.onTap, + onLongPress: widget.onLongPress, + leading: showCheckbox + ? FadeTransition( + opacity: _multiselectCheckboxAnim, + child: IgnorePointer( + ignoring: listUIState.isInMultiselect(), + child: Checkbox( + //key: NinjaKeys.expenseItemCheckbox(task.id), + value: widget.isChecked, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + onChanged: (value) => widget.onCheckboxChanged(value), + activeColor: Theme.of(context).accentColor, + ), + ), ) : null, title: Container( @@ -103,16 +130,16 @@ class ExpenseListItem extends StatelessWidget { children: [ Expanded( child: Text( - expense.publicNotes.isNotEmpty - ? expense.publicNotes - : formatDate(expense.expenseDate, context), + widget.expense.publicNotes.isNotEmpty + ? widget.expense.publicNotes + : formatDate(widget.expense.expenseDate, context), overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.title, ), ), Text( - formatNumber(expense.amountWithTax, context, - currencyId: expense.expenseCurrencyId), + formatNumber(widget.expense.amountWithTax, context, + currencyId: widget.expense.expenseCurrencyId), style: Theme.of(context).textTheme.title) ], ), @@ -131,16 +158,37 @@ class ExpenseListItem extends StatelessWidget { ) : Container(), ), - Text(localization.lookup('expense_status_${expense.statusId}'), + Text( + localization + .lookup('expense_status_${widget.expense.statusId}'), style: TextStyle( - color: ExpenseStatusColors.colors[expense.statusId], + color: + ExpenseStatusColors.colors[widget.expense.statusId], )), ], ), - EntityStateLabel(expense), + EntityStateLabel(widget.expense), ], ), ), ); } + + Animation _multiselectCheckboxAnim; + AnimationController _multiselectCheckboxAnimController; + + @override + void initState() { + super.initState(); + _multiselectCheckboxAnimController = + AnimationController(vsync: this, duration: Duration(milliseconds: 500)); + _multiselectCheckboxAnim = Tween(begin: 0.0, end: 1.0) + .animate(_multiselectCheckboxAnimController); + } + + @override + void dispose() { + _multiselectCheckboxAnimController.dispose(); + super.dispose(); + } } diff --git a/lib/ui/expense/expense_list_vm.dart b/lib/ui/expense/expense_list_vm.dart index b8df67535..44864dfcc 100644 --- a/lib/ui/expense/expense_list_vm.dart +++ b/lib/ui/expense/expense_list_vm.dart @@ -98,7 +98,7 @@ class ExpenseListVM { }, onEntityAction: (BuildContext context, List expenses, EntityAction action) => - handleExpenseAction(context, expenses[0], action), + handleExpenseAction(context, expenses, action), onRefreshed: (context) => _handleRefresh(context), ); } diff --git a/lib/ui/expense/expense_screen.dart b/lib/ui/expense/expense_screen.dart index 154da6290..8a0a6c4b0 100644 --- a/lib/ui/expense/expense_screen.dart +++ b/lib/ui/expense/expense_screen.dart @@ -1,19 +1,29 @@ import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:invoiceninja_flutter/constants.dart'; -import 'package:invoiceninja_flutter/ui/app/app_scaffold.dart'; -import 'package:invoiceninja_flutter/ui/app/list_filter.dart'; -import 'package:invoiceninja_flutter/ui/app/list_filter_button.dart'; -import 'package:invoiceninja_flutter/utils/localization.dart'; -import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; -import 'package:invoiceninja_flutter/ui/expense/expense_list_vm.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/redux/expense/expense_actions.dart'; import 'package:invoiceninja_flutter/ui/app/app_bottom_bar.dart'; +import 'package:invoiceninja_flutter/ui/app/app_scaffold.dart'; +import 'package:invoiceninja_flutter/ui/app/entities/entity_actions_dialog.dart'; +import 'package:invoiceninja_flutter/ui/app/list_filter.dart'; +import 'package:invoiceninja_flutter/ui/app/list_filter_button.dart'; +import 'package:invoiceninja_flutter/ui/expense/expense_list_vm.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; + +import 'expense_screen_vm.dart'; class ExpenseScreen extends StatelessWidget { + const ExpenseScreen({ + Key key, + @required this.viewModel, + }) : super(key: key); + static const String route = '/expense'; + final ExpenseScreenVM viewModel; + @override Widget build(BuildContext context) { final store = StoreProvider.of(context); @@ -21,8 +31,22 @@ class ExpenseScreen extends StatelessWidget { final company = state.selectedCompany; final userCompany = state.userCompany; final localization = AppLocalization.of(context); + final listUIState = state.uiState.expenseUIState.listUIState; + final isInMultiselect = listUIState.isInMultiselect(); return AppScaffold( + isChecked: isInMultiselect && + listUIState.selectedEntities.length == viewModel.expenseList.length, + showCheckbox: isInMultiselect, + onCheckboxChanged: (value) { + final expenses = viewModel.expenseList + .map((expenseId) => viewModel.expenseMap[expenseId]) + .where((expense) => value != listUIState.isSelected(expense)) + .toList(); + + viewModel.onEntityAction( + context, expenses, EntityAction.toggleMultiselect); + }, appBarTitle: ListFilter( key: ValueKey(store.state.expenseListState.filterClearedAt), entityType: EntityType.expense, @@ -31,12 +55,44 @@ class ExpenseScreen extends StatelessWidget { }, ), appBarActions: [ - ListFilterButton( - entityType: EntityType.expense, - onFilterPressed: (String value) { - store.dispatch(FilterExpenses(value)); - }, - ), + if (!viewModel.isInMultiselect) + ListFilterButton( + entityType: EntityType.expense, + onFilterPressed: (String value) { + store.dispatch(FilterExpenses(value)); + }, + ), + if (viewModel.isInMultiselect) + FlatButton( + key: key, + child: Text( + localization.cancel, + style: TextStyle(color: Colors.white), + ), + onPressed: () { + store.dispatch(ClearExpenseMultiselect(context: context)); + }, + ), + if (viewModel.isInMultiselect) + FlatButton( + key: key, + textColor: Colors.white, + disabledTextColor: Colors.white54, + child: Text( + localization.done, + ), + onPressed: state.expenseListState.selectedEntities.isEmpty + ? null + : () async { + await showEntityActionsDialog( + entities: state.expenseListState.selectedEntities, + userCompany: userCompany, + context: context, + onEntityAction: viewModel.onEntityAction, + multiselect: true); + store.dispatch(ClearExpenseMultiselect(context: context)); + }, + ), ], body: ExpenseListBuilder(), bottomNavigationBar: AppBottomBar( diff --git a/lib/ui/expense/expense_screen_vm.dart b/lib/ui/expense/expense_screen_vm.dart new file mode 100644 index 000000000..87c33e3f8 --- /dev/null +++ b/lib/ui/expense/expense_screen_vm.dart @@ -0,0 +1,64 @@ +import 'package:built_collection/built_collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/redux/expense/expense_actions.dart'; +import 'package:invoiceninja_flutter/redux/expense/expense_selectors.dart'; +import 'package:redux/redux.dart'; + +import 'expense_screen.dart'; + +class ExpenseScreenBuilder extends StatelessWidget { + const ExpenseScreenBuilder({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return StoreConnector( + //rebuildOnChange: true, + converter: ExpenseScreenVM.fromStore, + builder: (context, vm) { + return ExpenseScreen( + viewModel: vm, + ); + }, + ); + } +} + +class ExpenseScreenVM { + ExpenseScreenVM({ + @required this.isInMultiselect, + @required this.expenseList, + @required this.userCompany, + @required this.onEntityAction, + @required this.expenseMap, + }); + + final bool isInMultiselect; + final UserCompanyEntity userCompany; + final List expenseList; + final Function(BuildContext, List, EntityAction) onEntityAction; + final BuiltMap expenseMap; + + static ExpenseScreenVM fromStore(Store store) { + final state = store.state; + + return ExpenseScreenVM( + expenseMap: state.expenseState.map, + expenseList: memoizedFilteredExpenseList( + state.expenseState.map, + state.clientState.map, + state.vendorState.map, + state.expenseState.list, + state.expenseListState), + userCompany: state.userCompany, + isInMultiselect: state.expenseListState.isInMultiselect(), + onEntityAction: (BuildContext context, List expenses, + EntityAction action) => + handleExpenseAction(context, expenses, action), + ); + } +} diff --git a/lib/ui/expense/view/expense_view_vm.dart b/lib/ui/expense/view/expense_view_vm.dart index c7e1b0b99..47b4f0a53 100644 --- a/lib/ui/expense/view/expense_view_vm.dart +++ b/lib/ui/expense/view/expense_view_vm.dart @@ -146,7 +146,7 @@ class ExpenseViewVM { } }, onEntityAction: (BuildContext context, EntityAction action) => - handleExpenseAction(context, expense, action), + handleExpenseAction(context, [expense], action), onUploadDocument: (BuildContext context, String path) { final Completer completer = Completer();