// Dart imports: import 'dart:async'; import 'dart:math'; // Flutter imports: import 'package:built_collection/built_collection.dart'; import 'package:collection/collection.dart' show IterableNullableExtension; import 'package:flutter/material.dart'; // Package imports: import 'package:flutter_redux/flutter_redux.dart'; import 'package:overflow_view/overflow_view.dart'; // Project imports: import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/redux/ui/pref_state.dart'; import 'package:invoiceninja_flutter/ui/app/entities/entity_actions_dialog.dart'; import 'package:invoiceninja_flutter/ui/app/forms/save_cancel_buttons.dart'; import 'package:invoiceninja_flutter/ui/app/help_text.dart'; import 'package:invoiceninja_flutter/ui/app/icon_text.dart'; import 'package:invoiceninja_flutter/ui/app/lists/list_divider.dart'; import 'package:invoiceninja_flutter/ui/app/lists/list_filter.dart'; import 'package:invoiceninja_flutter/ui/app/loading_indicator.dart'; import 'package:invoiceninja_flutter/ui/app/presenters/entity_presenter.dart'; import 'package:invoiceninja_flutter/ui/app/scrollable_listview.dart'; import 'package:invoiceninja_flutter/ui/app/tables/entity_datatable.dart'; import 'package:invoiceninja_flutter/utils/icons.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/platforms.dart'; class EntityList extends StatefulWidget { EntityList({ required this.state, required this.entityType, required this.entityList, required this.onRefreshed, required this.onSortColumn, required this.itemBuilder, required this.onClearMultiselect, this.presenter, this.tableColumns, }) : super( key: ValueKey( '__${entityType}_${tableColumns}_${state.uiState.filterEntityId}_${state.getUIState(entityType)!.listUIState.tableHashCode}__')); final AppState state; final EntityType entityType; final List? tableColumns; final List entityList; final Function(BuildContext) onRefreshed; final EntityPresenter? presenter; final Function(String) onSortColumn; final Function(BuildContext, int) itemBuilder; final Function onClearMultiselect; @override _EntityListState createState() => _EntityListState(); } class _EntityListState extends State { late EntityDataTableSource dataTableSource; int _firstRowIndex = 0; @override void initState() { super.initState(); final entityType = widget.entityType; final state = widget.state; final entityList = widget.entityList; final entityMap = state.getEntityMap(entityType); final entityState = state.getUIState(entityType)!; dataTableSource = EntityDataTableSource( context: context, entityType: entityType, editingId: entityState.editingId, tableColumns: widget.tableColumns, entityList: entityList.toList(), entityMap: entityMap as BuiltMap?, entityPresenter: widget.presenter, onTap: (BaseEntity entity) => selectEntity(entity: entity), ); // make sure the initial page shows the selected record final entityUIState = state.getUIState(entityType); final rowsPerPage = state.prefState.rowsPerPage; if (widget.entityList.isNotEmpty) { if ((entityUIState!.selectedId ?? '').isNotEmpty) { final selectedIndex = widget.entityList.indexOf(entityUIState.selectedId); if (selectedIndex >= 0) { _firstRowIndex = (selectedIndex / rowsPerPage).floor() * rowsPerPage; } } else if (state.historyList.isNotEmpty) { final history = state.historyList.first; if (history.page != null) { _firstRowIndex = history.page! * rowsPerPage; } } } } @override void didUpdateWidget(EntityList oldWidget) { super.didUpdateWidget(oldWidget); final state = widget.state; final uiState = state.getUIState(widget.entityType)!; dataTableSource.editingId = uiState.editingId; dataTableSource.entityList = widget.entityList; dataTableSource.entityMap = state.getEntityMap(widget.entityType) as BuiltMap?; // ignore: invalid_use_of_visible_for_testing_member, invalid_use_of_protected_member dataTableSource.notifyListeners(); } @override Widget build(BuildContext context) { final store = StoreProvider.of(context); final localization = AppLocalization.of(context); final state = widget.state; final uiState = state.uiState; final entityType = widget.entityType; final listUIState = state.getUIState(entityType)!.listUIState; final isInMultiselect = listUIState.isInMultiselect(); final entityList = widget.entityList; final entityMap = state.getEntityMap(entityType); final countSelected = (listUIState.selectedIds ?? []).length; final isList = entityType.isSetting || state.prefState.isModuleList; if (!state.isLoaded && entityList.isEmpty) { return LoadingIndicator(); } final shouldSelectEntity = state.shouldSelectEntity( entityType: entityType, entityList: entityList); if (shouldSelectEntity != false) { // null is a special case which means we need to reselect // the current selection to add it to the history final entityId = shouldSelectEntity == null ? state.getUIState(entityType)!.selectedId : (entityList.isEmpty ? null : entityList.first); WidgetsBinding.instance.addPostFrameCallback((duration) { viewEntityById( entityType: entityType, entityId: entityId, ); }); } final listOrTable = () { if (isList) { return Column( mainAxisSize: MainAxisSize.min, children: [ if (uiState.filterEntityId != null && isMobile(context)) ListFilterMessage( filterEntityId: uiState.filterEntityId, filterEntityType: uiState.filterEntityType, onPressed: (_) => viewEntityById( entityId: state.uiState.filterEntityId, entityType: state.uiState.filterEntityType), onClearPressed: () => store.dispatch(ClearEntityFilter()), ), Expanded( child: entityList.isEmpty ? HelpText( AppLocalization.of(context)!.clickPlusToCreateRecord) : ScrollableListViewBuilder( primary: true, padding: const EdgeInsets.symmetric(vertical: 20), separatorBuilder: (context, index) => (index == 0 || index == entityList.length) ? SizedBox() : ListDivider(), itemCount: entityList.length + 2, itemBuilder: (BuildContext context, index) { if (index == 0 || index == entityList.length + 1) { return Container( color: Theme.of(context).cardColor, height: 25, ); } else { return widget.itemBuilder(context, index - 1); } }, ) /*DraggableScrollbar.semicircle( backgroundColor: Theme.of(context).backgroundColor, scrollbarTimeToFade: Duration(seconds: 1), controller: _scrollController, child: ScrollableListViewBuilder( padding: const EdgeInsets.symmetric(vertical: 25), controller: _scrollController, separatorBuilder: (context, index) => (index == 0 || index == entityList.length) ? SizedBox() : ListDivider(), itemCount: entityList.length + 2, itemBuilder: (BuildContext context, index) { if (index == 0 || index == entityList.length + 1) { return Container( color: Theme.of(context).cardColor, height: 25, ); } else { return widget.itemBuilder(context, index - 1); } }, ), )*/ , ), ], ); } else { final rowsPerPage = state.prefState.rowsPerPage; return Column( mainAxisSize: MainAxisSize.max, children: [ if (uiState.filterEntityId != null && isMobile(context)) ListFilterMessage( filterEntityId: uiState.filterEntityId, filterEntityType: uiState.filterEntityType, onPressed: (_) { viewEntityById( entityId: state.uiState.filterEntityId, entityType: state.uiState.filterEntityType); }, onClearPressed: () { store.dispatch(ClearEntityFilter()); }, ), Expanded( child: SingleChildScrollView( primary: true, child: Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: PaginatedDataTable( //hasActionsColumn: true, onSelectAll: (value) { final startIndex = min(_firstRowIndex, entityList.length - 1); final endIndex = min(_firstRowIndex + rowsPerPage, entityList.length); final entities = entityList .sublist(startIndex, endIndex) .map((String? entityId) => entityMap![entityId] as BaseEntity) .where((invoice) => value != listUIState.isSelected(invoice.id)) .toList(); handleEntitiesActions( entities, EntityAction.toggleMultiselect); }, columns: [ if (!isInMultiselect) DataColumn(label: SizedBox()), ...widget.tableColumns!.map((field) { String? label = AppLocalization.of(context)!.lookup(field); if (field.startsWith('custom')) { final key = field.replaceFirst( 'custom', entityType.snakeCase); label = state.company.getCustomFieldLabel(key); } return DataColumn( label: Container( child: Text( label, overflow: TextOverflow.ellipsis, ), ), onSort: (int columnIndex, bool ascending) { widget.onSortColumn(field); }); }), ], source: dataTableSource, sortColumnIndex: widget.tableColumns! .contains(listUIState.sortField) ? widget.tableColumns!.indexOf(listUIState.sortField) : 0, sortAscending: listUIState.sortAscending, rowsPerPage: state.prefState.rowsPerPage, onPageChanged: (row) { _firstRowIndex = row; store.dispatch(UpdateLastHistory( (row / state.prefState.rowsPerPage).floor())); }, initialFirstRowIndex: _firstRowIndex, availableRowsPerPage: [ 10, 25, 50, 100, ], onRowsPerPageChanged: (value) { store.dispatch(UpdateUserPreferences(rowsPerPage: value)); }, ), ), ), ), ], ); } }; final entities = listUIState.selectedIds == null ? [] : listUIState.selectedIds! .map((entityId) => entityMap![entityId] as BaseEntity) .toList(); final firstEntity = entities.isEmpty ? null : entities.first; final actions = (firstEntity?.getActions( includeEdit: false, multiselect: true, userCompany: state.userCompany, client: (firstEntity is BelongsToClient) ? state.clientState .get((firstEntity as BelongsToClient).clientId!) : null, ) ?? []) .whereNotNull(); return RefreshIndicator( onRefresh: () => widget.onRefreshed(context), child: Column( children: [ AnimatedContainer( padding: const EdgeInsets.symmetric(horizontal: 10), color: Theme.of(context).cardColor, height: isInMultiselect ? kTopBottomBarHeight : 0, duration: Duration(milliseconds: kDefaultAnimationDuration), curve: Curves.easeInOutCubic, child: AnimatedOpacity( opacity: isInMultiselect ? 1 : 0, duration: Duration(milliseconds: kDefaultAnimationDuration), curve: Curves.easeInOutCubic, child: Row( children: [ if (state.prefState.moduleLayout == ModuleLayout.list || entityType.isSetting) Checkbox( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, onChanged: (value) { final endIndex = min(entityList.length, kMaxEntitiesPerBulkAction); final entities = entityList .sublist(0, endIndex) .map((entityId) => entityMap![entityId] as BaseEntity) .toList(); handleEntitiesActions( entities, EntityAction.toggleMultiselect); }, activeColor: Theme.of(context).colorScheme.secondary, value: entityList.length == (listUIState.selectedIds ?? []).length, ), if (isDesktop(context)) ...[ Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: Text(isList ? '($countSelected)' : localization!.countSelected .replaceFirst(':count', '$countSelected')), ), Expanded( child: Align( alignment: Alignment.centerRight, child: OverflowView.flexible( spacing: 8, children: actions .map( (action) => OutlinedButton( child: IconText( icon: getEntityActionIcon(action), text: localization!.lookup('$action'), ), onPressed: () { handleEntitiesActions(entities, action); widget.onClearMultiselect(); }, ), ) .toList(), builder: (context, remaining) { return PopupMenuButton( child: Padding( padding: const EdgeInsets.symmetric( horizontal: 8), child: Row( children: [ Text( localization!.more, style: TextStyle( color: state.prefState.enableDarkMode ? Colors.white : Colors.black), ), SizedBox(width: 4), Icon(Icons.arrow_drop_down, color: state.prefState.enableDarkMode ? Colors.white : Colors.black), ], ), ), onSelected: (EntityAction action) { handleEntitiesActions(entities, action); widget.onClearMultiselect(); }, itemBuilder: (BuildContext context) { return actions .toList() .sublist(actions.length - remaining) .map((action) { return PopupMenuItem( value: action, child: Row( children: [ Icon(getEntityActionIcon(action), color: Theme.of(context) .colorScheme .secondary), SizedBox(width: 16.0), Text(AppLocalization.of(context)! .lookup(action.toString())), ], ), ); }).toList(); }, ); }), ), ) ] else ...[ SizedBox(width: 16), Expanded( child: Text(localization!.countSelected .replaceFirst(':count', '$countSelected')), ), SaveCancelButtons( isHeader: false, saveLabel: localization.actions, isEnabled: entities.isNotEmpty, isCancelEnabled: true, onSavePressed: (context) async { await showEntityActionsDialog( entities: entities, multiselect: true, completer: Completer() ..future.then( (_) => widget.onClearMultiselect()), ); }, onCancelPressed: (_) => widget.onClearMultiselect(), ), ] ], ), ), ), Expanded( child: Stack( alignment: Alignment.topCenter, children: [ listOrTable(), if ((state.isLoading && (isMobile(context) || !entityType.isSetting)) || (state.isSaving && (entityType.isSetting || (!state.prefState.isPreviewVisible && !state.uiState.isEditing)))) LinearProgressIndicator(), ], ), ), ], )); } }