diff --git a/lib/business_logic/item_action.dart b/lib/business_logic/item_action.dart index 239441f..a4f5004 100644 --- a/lib/business_logic/item_action.dart +++ b/lib/business_logic/item_action.dart @@ -11,7 +11,7 @@ import 'package:spare_parts/utilities/constants.dart'; import 'package:spare_parts/utilities/helpers.dart'; import 'package:spare_parts/widgets/dialogs/print_dialog/print_dialog_mobile.dart' if (dart.library.html) 'package:spare_parts/widgets/dialogs/print_dialog/print_dialog_web.dart'; -import 'package:spare_parts/widgets/inputs/user_selection_dialog.dart'; +import 'package:spare_parts/widgets/dialogs/user_selection_dialog.dart'; import 'package:spare_parts/widgets/inventory_list_item/inventory_item_form.dart'; import '../services/repositories/repositories.dart'; @@ -97,7 +97,7 @@ class AssignItemAction extends ItemAction { final users = await showDialog?>( context: context, builder: (context) => UserSelectionDialog( - title: 'Select user', + title: 'Select User', isSingleSelection: true, selectedUsers: item.borrower == null ? [] : [item.borrower!], ), diff --git a/lib/entities/custom_user.dart b/lib/entities/custom_user.dart index 650e06a..b92b94c 100644 --- a/lib/entities/custom_user.dart +++ b/lib/entities/custom_user.dart @@ -28,4 +28,14 @@ class CustomUser { 'photoURL': photoURL, }; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is CustomUser && other.uid == uid; + } + + @override + int get hashCode => uid.hashCode; } diff --git a/lib/factories/inventory_item_factory.dart b/lib/factories/inventory_item_factory.dart index ca200f0..cc079cf 100644 --- a/lib/factories/inventory_item_factory.dart +++ b/lib/factories/inventory_item_factory.dart @@ -1,13 +1,15 @@ +import 'package:spare_parts/entities/custom_user.dart'; import 'package:spare_parts/entities/inventory_item.dart'; import 'package:spare_parts/factories/factory.dart'; class InventoryItemFactory extends Factory { @override - InventoryItem create({String? name}) { + InventoryItem create({String? name, CustomUser? borrower}) { return InventoryItem( id: faker.guid.guid(), type: faker.company.suffix(), name: name ?? faker.company.name(), + borrower: borrower ); } } diff --git a/lib/pages/home_page/inventory_view/filters/available_items_filter.dart b/lib/pages/home_page/inventory_view/filters/available_items_filter.dart index af57e0a..840d3f1 100644 --- a/lib/pages/home_page/inventory_view/filters/available_items_filter.dart +++ b/lib/pages/home_page/inventory_view/filters/available_items_filter.dart @@ -1,17 +1,20 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:spare_parts/pages/home_page/inventory_view/filters/inventory_view_filter_selection.dart'; class AvailableItemsFilter extends StatelessWidget { - final bool value; - final void Function() onPressed; + const AvailableItemsFilter({super.key}); - const AvailableItemsFilter({ - super.key, - required this.value, - required this.onPressed, - }); + void onPressed(BuildContext context) { + final selection = context.read(); + selection.toggleShowOnlyAvailableItems(); + } @override Widget build(BuildContext context) { + final value = context.select( + (selection) => selection.showOnlyAvailableItems); + return TextButton.icon( label: Text('Only available items'), icon: Icon( @@ -20,7 +23,7 @@ class AvailableItemsFilter extends StatelessWidget { style: TextButton.styleFrom( foregroundColor: Theme.of(context).textTheme.bodyLarge!.color, ), - onPressed: onPressed, + onPressed: () => onPressed(context), ); } } diff --git a/lib/pages/home_page/inventory_view/filters/inventory_view_filter_selection.dart b/lib/pages/home_page/inventory_view/filters/inventory_view_filter_selection.dart new file mode 100644 index 0000000..2699e1a --- /dev/null +++ b/lib/pages/home_page/inventory_view/filters/inventory_view_filter_selection.dart @@ -0,0 +1,29 @@ +import 'package:flutter/foundation.dart'; +import 'package:spare_parts/entities/custom_user.dart'; + +class InventoryViewFilterSelection extends ChangeNotifier { + List selectedItemTypes = []; + List selectedBorrowers = []; + bool showOnlyAvailableItems = false; + + InventoryViewFilterSelection({ + this.selectedItemTypes = const [], + this.selectedBorrowers = const [], + this.showOnlyAvailableItems = false, + }); + + void updateSelectedItemTypes(List selectedItemTypes) { + this.selectedItemTypes = selectedItemTypes; + notifyListeners(); + } + + void updateSelectedBorrowers(List selectedBorrowers) { + this.selectedBorrowers = selectedBorrowers; + notifyListeners(); + } + + void toggleShowOnlyAvailableItems() { + showOnlyAvailableItems = !showOnlyAvailableItems; + notifyListeners(); + } +} diff --git a/lib/pages/home_page/inventory_view/filters/inventory_view_filters.dart b/lib/pages/home_page/inventory_view/filters/inventory_view_filters.dart new file mode 100644 index 0000000..b206ae0 --- /dev/null +++ b/lib/pages/home_page/inventory_view/filters/inventory_view_filters.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:spare_parts/pages/home_page/inventory_view/filters/available_items_filter.dart'; +import 'package:spare_parts/pages/home_page/inventory_view/filters/item_type_filter.dart'; +import 'package:spare_parts/pages/home_page/inventory_view/filters/user_filter.dart'; +import 'package:spare_parts/utilities/constants.dart'; + +class InventoryViewFilters extends StatelessWidget { + const InventoryViewFilters({ + super.key, + }); + + @override + Widget build(BuildContext context) { + bool isAdmin = context.watch() == UserRole.admin; + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + SizedBox(width: 5), + ItemTypeFilter(), + if (isAdmin) ...[ + SizedBox(width: 10), + UserFilter(), + SizedBox(width: 10), + AvailableItemsFilter(), + ], + ], + ), + ); + } +} diff --git a/lib/pages/home_page/inventory_view/filters/item_type_filter.dart b/lib/pages/home_page/inventory_view/filters/item_type_filter.dart new file mode 100644 index 0000000..f379bd3 --- /dev/null +++ b/lib/pages/home_page/inventory_view/filters/item_type_filter.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:spare_parts/pages/home_page/inventory_view/filters/inventory_view_filter_selection.dart'; +import 'package:spare_parts/utilities/constants.dart'; +import 'package:spare_parts/widgets/dialogs/value_selection_dialog.dart'; +import 'package:spare_parts/widgets/inputs/multiselect_button.dart'; + +class ItemTypeFilter extends StatelessWidget { + const ItemTypeFilter({ + super.key, + }); + + void onChanged(BuildContext context, List selectedItemTypes) { + final selection = context.read(); + selection.updateSelectedItemTypes(selectedItemTypes); + } + + @override + Widget build(BuildContext context) { + final selectedItemTypes = + context.select>( + (selection) => selection.selectedItemTypes); + + return MultiselectButton( + buttonLabel: 'Item Types', + hasSelection: selectedItemTypes.isNotEmpty, + onConfirm: (values) => onChanged(context, values), + dialog: ValueSelectionDialog( + values: itemTypes.keys.toList(), + selectedValues: selectedItemTypes, + title: 'Pick Item Types', + leadingBuilder: (itemType) => + Icon(itemTypes[itemType] ?? itemTypes['Other']!), + ), + ); + } +} diff --git a/lib/pages/home_page/inventory_view/filters/search_field.dart b/lib/pages/home_page/inventory_view/filters/search_field.dart deleted file mode 100644 index 36fb257..0000000 --- a/lib/pages/home_page/inventory_view/filters/search_field.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter/material.dart'; - -class SearchField extends StatelessWidget { - final void Function(String)? onChanged; - final TextEditingController searchFieldController; - - const SearchField({ - super.key, - required this.onChanged, - required this.searchFieldController, - }); - - @override - Widget build(BuildContext context) { - return TextField( - key: Key('search'), - controller: searchFieldController, - decoration: InputDecoration( - hintText: 'Search', - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(100)), - ), - suffixIcon: IconButton( - icon: Icon( - searchFieldController.text.isEmpty ? Icons.search : Icons.clear), - onPressed: () { - onChanged?.call(''); - searchFieldController.clear(); - }, - ), - ), - onChanged: onChanged, - enabled: onChanged != null, - ); - } -} diff --git a/lib/pages/home_page/inventory_view/filters/user_filter.dart b/lib/pages/home_page/inventory_view/filters/user_filter.dart index 1f90e65..d9597e7 100644 --- a/lib/pages/home_page/inventory_view/filters/user_filter.dart +++ b/lib/pages/home_page/inventory_view/filters/user_filter.dart @@ -1,65 +1,34 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:spare_parts/dtos/user_dto.dart'; -import 'package:spare_parts/services/callable_service.dart'; +import 'package:spare_parts/entities/custom_user.dart'; +import 'package:spare_parts/pages/home_page/inventory_view/filters/inventory_view_filter_selection.dart'; +import 'package:spare_parts/widgets/dialogs/user_selection_dialog.dart'; import 'package:spare_parts/widgets/inputs/multiselect_button.dart'; -import 'package:spare_parts/widgets/user_avatar.dart'; - -class UserFilter extends StatefulWidget { - final List selectedUsers; - final void Function(List) onChanged; - final IconData? icon; +class UserFilter extends StatelessWidget { const UserFilter({ - super.key, - required this.selectedUsers, - required this.onChanged, - this.icon, + super.key }); - @override - State createState() => _UserFilterState(); -} - -class _UserFilterState extends State { - late Future> _userQuery; - - @override - void initState() { - final callableService = context.read(); - _userQuery = callableService.getUsers(); - - super.initState(); + void onChanged(BuildContext context, List selectedUsers) { + final selection = context.read(); + selection.updateSelectedBorrowers(selectedUsers); } @override Widget build(BuildContext context) { - return FutureBuilder>( - future: _userQuery, - builder: (context, snap) { - if (!snap.hasData || snap.hasError) { - return SizedBox.square( - dimension: 20, - child: CircularProgressIndicator(strokeWidth: 3), - ); - } - - final users = snap.data!; - - return MultiselectButton( - buttonLabel: 'Borrowers', - values: users.map((u) => u.id).toList(), - selectedValues: widget.selectedUsers, - onConfirm: widget.onChanged, - icon: widget.icon, - labelBuilder: (uid) => - users.singleWhere((user) => user.id == uid).name, - leadingBuilder: (uid) { - final user = users.singleWhere((user) => user.id == uid); - return UserAvatar(photoUrl: user.photoUrl); - }, - ); - }, + final selectedUsers = + context.select>( + (selection) => selection.selectedBorrowers); + + return MultiselectButton( + buttonLabel: 'Borrowers', + hasSelection: selectedUsers.isNotEmpty, + onConfirm: (values) => onChanged(context, values), + dialog: UserSelectionDialog( + selectedUsers: selectedUsers, + title: 'Pick Borrowers', + ), ); } } diff --git a/lib/pages/home_page/inventory_view/inventory_view.dart b/lib/pages/home_page/inventory_view/inventory_view.dart index eaa585e..92d8c50 100644 --- a/lib/pages/home_page/inventory_view/inventory_view.dart +++ b/lib/pages/home_page/inventory_view/inventory_view.dart @@ -1,14 +1,12 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:spare_parts/entities/inventory_item.dart'; -import 'package:spare_parts/pages/home_page/inventory_view/filters/available_items_filter.dart'; -import 'package:spare_parts/pages/home_page/inventory_view/filters/search_field.dart'; -import 'package:spare_parts/pages/home_page/inventory_view/filters/user_filter.dart'; +import 'package:spare_parts/pages/home_page/inventory_view/filters/inventory_view_filter_selection.dart'; +import 'package:spare_parts/pages/home_page/inventory_view/filters/inventory_view_filters.dart'; import 'package:spare_parts/pages/home_page/inventory_view/inventory_view_list.dart'; import 'package:spare_parts/services/repositories/repositories.dart'; import 'package:spare_parts/utilities/constants.dart'; import 'package:spare_parts/widgets/error_container.dart'; -import 'package:spare_parts/widgets/inputs/multiselect_button.dart'; import 'package:spare_parts/widgets/inventory_list_item_loading.dart'; class InventoryView extends StatefulWidget { @@ -19,35 +17,11 @@ class InventoryView extends StatefulWidget { } class _InventoryViewState extends State { - List _selectedItemTypes = []; - List _selectedBorrowers = []; - late bool _showOnlyAvailableItems; - String _searchQuery = ''; - final searchFieldController = TextEditingController(); bool _inSelectionMode = false; int _itemsLimit = kItemsPerPage; bool get isAdmin => context.read() == UserRole.admin; - @override - void initState() { - _showOnlyAvailableItems = !isAdmin; - - super.initState(); - } - - void _handleTypesFilterChanged(List newTypes) { - setState(() => _selectedItemTypes = newTypes); - } - - void _handleBorrowersFilterChanged(List newBorrowers) { - setState(() => _selectedBorrowers = newBorrowers); - } - - void _handleAvailableItemsFilterChanged() { - setState(() => _showOnlyAvailableItems = !_showOnlyAvailableItems); - } - void _handleSelectionModeChanged(bool inSelectionMode) { setState(() => _inSelectionMode = inSelectionMode); } @@ -60,99 +34,68 @@ class _InventoryViewState extends State { Widget build(BuildContext context) { final inventoryItemRepository = context.read(); - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: SearchField( - searchFieldController: searchFieldController, - onChanged: _inSelectionMode - ? null - : (value) => setState(() { - _searchQuery = value; - }), - ), - ), - ), + return ChangeNotifierProvider( + create: (_) => InventoryViewFilterSelection( + showOnlyAvailableItems: !isAdmin, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!_inSelectionMode) ...[ + InventoryViewFilters(), + Divider(), ], - ), - if (!_inSelectionMode) ...[ - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - MultiselectButton( - buttonLabel: 'Item Types', - values: itemTypes.keys.toList(), - selectedValues: _selectedItemTypes, - icon: Icons.filter_list, - leadingBuilder: (itemType) => - Icon(itemTypes[itemType] ?? itemTypes['Other']!), - onConfirm: _handleTypesFilterChanged, - ), - if (isAdmin) ...[ - SizedBox(width: 10), - UserFilter( - icon: Icons.filter_list, - selectedUsers: _selectedBorrowers, - onChanged: _handleBorrowersFilterChanged, - ), - SizedBox(width: 10), - AvailableItemsFilter( - value: _showOnlyAvailableItems, - onPressed: _handleAvailableItemsFilterChanged, + Expanded( + child: Builder( + builder: (context) { + final filterSelection = + context.watch(); + return StreamBuilder>( + stream: inventoryItemRepository.getItemsStream( + withNoBorrower: filterSelection.showOnlyAvailableItems, + whereTypeIn: filterSelection.selectedItemTypes.isEmpty + ? null + : filterSelection.selectedItemTypes, + whereBorrowerIn: filterSelection.selectedBorrowers.isEmpty + ? null + : filterSelection.selectedBorrowers + .map((e) => e.uid) + .toList(), + excludePrivates: !isAdmin, + limit: _itemsLimit, ), - ], - ], + builder: (context, snapshot) { + if (snapshot.hasError) { + debugPrint(snapshot.error.toString()); + debugPrintStack(stackTrace: snapshot.stackTrace); + return ErrorContainer(error: snapshot.error.toString()); + } + + if (!snapshot.hasData) { + return ListView( + children: List.generate( + 10, + (index) => + InventoryListItemLoading(hasAuthor: isAdmin), + ), + ); + } + + final items = snapshot.data!; + + return InventoryViewList( + items: items, + loadedAllItems: items.length < _itemsLimit, + onSelectionModeChanged: _handleSelectionModeChanged, + onLoadMore: _loadMoreItems, + ); + }, + ); + }, ), ), - Divider(), ], - Expanded( - child: StreamBuilder>( - stream: inventoryItemRepository.getItemsStream( - withNoBorrower: - _selectedBorrowers.isEmpty && _showOnlyAvailableItems, - whereTypeIn: - _selectedItemTypes.isEmpty ? null : _selectedItemTypes, - whereBorrowerIn: - _selectedBorrowers.isEmpty ? null : _selectedBorrowers, - excludePrivates: !isAdmin, - limit: _itemsLimit, - ), - builder: (context, snapshot) { - if (snapshot.hasError) { - debugPrint(snapshot.error.toString()); - debugPrintStack(stackTrace: snapshot.stackTrace); - return ErrorContainer(error: snapshot.error.toString()); - } - - if (!snapshot.hasData) { - return ListView( - children: List.generate( - 10, - (index) => InventoryListItemLoading(hasAuthor: isAdmin), - ), - ); - } - - final items = snapshot.data!; - - return InventoryViewList( - items: items, - searchQuery: _searchQuery, - loadedAllItems: items.length < _itemsLimit, - onSelectionModeChanged: _handleSelectionModeChanged, - onLoadMore: _loadMoreItems, - ); - }, - ), - ), - ], + ), ); } } diff --git a/lib/pages/home_page/inventory_view/inventory_view_list.dart b/lib/pages/home_page/inventory_view/inventory_view_list.dart index 4b41df5..64d14ca 100644 --- a/lib/pages/home_page/inventory_view/inventory_view_list.dart +++ b/lib/pages/home_page/inventory_view/inventory_view_list.dart @@ -8,7 +8,6 @@ import 'package:spare_parts/widgets/inventory_list_item.dart'; class InventoryViewList extends StatefulWidget { final List items; - final String? searchQuery; final bool loadedAllItems; final void Function(bool)? onSelectionModeChanged; final void Function()? onLoadMore; @@ -16,7 +15,6 @@ class InventoryViewList extends StatefulWidget { const InventoryViewList({ super.key, required this.items, - this.searchQuery, this.onSelectionModeChanged, this.onLoadMore, this.loadedAllItems = true, @@ -76,17 +74,7 @@ class _InventoryViewListState extends State { ); } - final filteredItems = widget.items.where((item) { - if (widget.searchQuery == null) { - return true; - } - - List properties = [item.name, item.borrower?.name]; - return properties.where((property) => property != null).any((property) => - property!.toLowerCase().contains(widget.searchQuery!.toLowerCase())); - }).toList(); - - filteredItems.sort(); + widget.items.sort(); return Column( mainAxisSize: MainAxisSize.min, @@ -101,10 +89,10 @@ class _InventoryViewListState extends State { ], Expanded( child: ListView.builder( - itemCount: filteredItems.length + 1, + itemCount: widget.items.length + 1, shrinkWrap: true, itemBuilder: (BuildContext context, int index) { - if (index == filteredItems.length) { + if (index == widget.items.length) { if (widget.loadedAllItems) { return SizedBox.shrink(); } @@ -118,7 +106,7 @@ class _InventoryViewListState extends State { ); } - final item = filteredItems[index]; + final item = widget.items[index]; return InventoryListItem( item: item, showBorrower: true, diff --git a/lib/pages/home_page/settings_view/borrowing_rules_setting/item_type_edit_button.dart b/lib/pages/home_page/settings_view/borrowing_rules_setting/item_type_edit_button.dart index 7b393c9..1ca2e19 100644 --- a/lib/pages/home_page/settings_view/borrowing_rules_setting/item_type_edit_button.dart +++ b/lib/pages/home_page/settings_view/borrowing_rules_setting/item_type_edit_button.dart @@ -3,7 +3,7 @@ import 'package:provider/provider.dart'; import 'package:spare_parts/entities/borrowing_rule.dart'; import 'package:spare_parts/services/repositories/repositories.dart'; import 'package:spare_parts/utilities/constants.dart'; -import 'package:spare_parts/widgets/inputs/value_selection_dialog.dart'; +import 'package:spare_parts/widgets/dialogs/value_selection_dialog.dart'; class ItemTypeEditButton extends StatelessWidget { final List existingRules; @@ -26,11 +26,10 @@ class ItemTypeEditButton extends StatelessWidget { context: context, builder: (context) => ValueSelectionDialog( isSingleSelection: true, - title: 'Select user', + title: 'Select Item Type', values: itemTypes.keys.toList(), selectedValues: itemTypes.keys.where((type) => type == rule.type).toList(), - labelBuilder: (type) => type, disabledValues: existingRules .where((otherRule) => otherRule != rule) .map((rule) => rule.type) diff --git a/lib/pages/home_page/settings_view/set_admins_button.dart b/lib/pages/home_page/settings_view/set_admins_button.dart index d7a5d2b..bffea2a 100644 --- a/lib/pages/home_page/settings_view/set_admins_button.dart +++ b/lib/pages/home_page/settings_view/set_admins_button.dart @@ -4,7 +4,7 @@ import 'package:provider/provider.dart'; import 'package:spare_parts/services/callable_service.dart'; import 'package:spare_parts/utilities/constants.dart'; import 'package:spare_parts/utilities/helpers.dart'; -import 'package:spare_parts/widgets/inputs/value_selection_dialog.dart'; +import 'package:spare_parts/widgets/dialogs/value_selection_dialog.dart'; import 'package:spare_parts/widgets/title_text.dart'; import 'package:spare_parts/widgets/user_avatar.dart'; diff --git a/lib/services/repositories/inventory_item_repository.dart b/lib/services/repositories/inventory_item_repository.dart index 0ef5914..e9c7229 100644 --- a/lib/services/repositories/inventory_item_repository.dart +++ b/lib/services/repositories/inventory_item_repository.dart @@ -61,6 +61,54 @@ class InventoryItemRepository extends FirestoreService { .map(_mapQuerySnapshotToInventoryItems); } + /// This is an improved implementation of the method above + /// We can't use it right now, because testing `OR` queries with fake_cloud_firestore is hard + /// Related issue: https://github.com/atn832/fake_cloud_firestore/issues/293 + Stream> getItemsStreamWithFilters({ + bool? withNoBorrower, + String? whereBorrowerIs, + List? whereBorrowerIn, + List? whereTypeIn, + bool excludePrivates = false, + int limit = kItemsPerPage, + }) { + List filters = []; + + if (withNoBorrower != null && withNoBorrower) { + filters.add(Filter('borrower', isNull: true)); + } + + if (whereBorrowerIs != null) { + filters.add(Filter('borrower.uid', isEqualTo: whereBorrowerIs)); + } + + if (whereBorrowerIn != null) { + filters.add(Filter('borrower.uid', whereIn: whereBorrowerIn)); + } + if (whereTypeIn != null) { + filters.add(Filter('type', whereIn: whereTypeIn)); + } + + if (excludePrivates) { + filters.add(Filter('isPrivate', isEqualTo: false)); + } + + Query query = itemsCollection; + + if (filters.isNotEmpty) { + Filter andFilter = filters[0]; + for (int i = 1; i < filters.length; i++) { + andFilter = Filter.and(andFilter, filters[i]); + } + query = itemsCollection.where(andFilter); + } + + return query + .orderBy('name') + .limit(limit) + .snapshots() + .map(_mapQuerySnapshotToInventoryItems); + } Future delete(String? itemId) async { await getItemDocumentReference(itemId).delete(); } diff --git a/lib/utilities/helpers.dart b/lib/utilities/helpers.dart index ccd2384..38cf7f2 100644 --- a/lib/utilities/helpers.dart +++ b/lib/utilities/helpers.dart @@ -8,7 +8,9 @@ import 'package:intl/intl.dart'; void showError({required BuildContext context, required String message}) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message), backgroundColor: Theme.of(context).colorScheme.error), + SnackBar( + content: Text(message), + backgroundColor: Theme.of(context).colorScheme.error), ); } @@ -40,3 +42,5 @@ String formatDate(DateTime dateTime, {bool withTime = true}) { } return dateTimeFormat.format(dateTime); } + +String stringIdentity(dynamic value) => value.toString(); diff --git a/lib/widgets/dialogs/dialog_width.dart b/lib/widgets/dialogs/dialog_width.dart new file mode 100644 index 0000000..e90211b --- /dev/null +++ b/lib/widgets/dialogs/dialog_width.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class DialogWidth extends StatelessWidget { + final Widget child; + + const DialogWidth({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return SizedBox(width: 300, child: child); + } +} diff --git a/lib/widgets/dialogs/user_selection_dialog.dart b/lib/widgets/dialogs/user_selection_dialog.dart new file mode 100644 index 0000000..ff6c917 --- /dev/null +++ b/lib/widgets/dialogs/user_selection_dialog.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:spare_parts/entities/custom_user.dart'; +import 'package:spare_parts/services/repositories/user_repository.dart'; +import 'package:spare_parts/widgets/dialogs/dialog_width.dart'; +import 'package:spare_parts/widgets/dialogs/value_selection_dialog.dart'; +import 'package:spare_parts/widgets/inputs/new_user_input.dart'; +import 'package:spare_parts/widgets/user_avatar.dart'; + +class UserSelectionDialog extends StatefulWidget { + final List selectedUsers; + final List disabledUids; + final String title; + final bool isSingleSelection; + + const UserSelectionDialog({ + super.key, + required this.selectedUsers, + required this.title, + this.isSingleSelection = false, + this.disabledUids = const [], + }); + + @override + State createState() => _UserSelectionDialogState(); +} + +class _UserSelectionDialogState extends State { + UserRepository get userRepository => context.read(); + + @override + Widget build(BuildContext context) { + return StreamBuilder>( + stream: userRepository.getAllStream(), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return AlertDialog( + content: + DialogWidth(child: Center(child: CircularProgressIndicator())), + ); + } + + final users = snapshot.data!; + final disabledUsers = widget.disabledUids + .map((uid) => users.singleWhere((user) => user.uid == uid)) + .toList(); + + return ValueSelectionDialog( + key: Key(users.map((u) => u.uid).join(',')), + selectedValues: widget.selectedUsers, + title: widget.title, + values: users, + isSingleSelection: widget.isSingleSelection, + leadingBuilder: (user) => UserAvatar(photoUrl: user.photoURL), + labelBuilder: (user) => user.name ?? '', + disabledValues: disabledUsers, + trailing: NewUserInput(), + ); + }, + ); + } +} diff --git a/lib/widgets/inputs/value_selection_dialog.dart b/lib/widgets/dialogs/value_selection_dialog.dart similarity index 58% rename from lib/widgets/inputs/value_selection_dialog.dart rename to lib/widgets/dialogs/value_selection_dialog.dart index 398b5f7..8d3c272 100644 --- a/lib/widgets/inputs/value_selection_dialog.dart +++ b/lib/widgets/dialogs/value_selection_dialog.dart @@ -1,63 +1,84 @@ import 'package:flutter/material.dart'; +import 'package:spare_parts/utilities/helpers.dart'; +import 'package:spare_parts/widgets/dialogs/dialog_width.dart'; +import 'package:spare_parts/widgets/inputs/search_field.dart'; -class ValueSelectionDialog extends StatefulWidget { - final List selectedValues; - final List values; - final List disabledValues; +class ValueSelectionDialog extends StatefulWidget { + final List selectedValues; + final List values; + final List disabledValues; final String title; final bool isSingleSelection; - final Widget Function(String value)? leadingBuilder; - final String Function(String value)? labelBuilder; + final Widget Function(T value)? leadingBuilder; + final String Function(T value) labelBuilder; + final Widget? trailing; const ValueSelectionDialog({ super.key, required this.selectedValues, required this.title, required this.values, + this.labelBuilder = stringIdentity, this.isSingleSelection = false, this.leadingBuilder, - this.labelBuilder, this.disabledValues = const [], + this.trailing, }); @override - State createState() => _ValueSelectionDialogState(); + State> createState() => + _ValueSelectionDialogState(); } -class _ValueSelectionDialogState extends State { - late final List _newSelectedValues; +class _ValueSelectionDialogState extends State> { + late final List _allValues; + late final List _newSelectedValues; + String _searchQuery = ''; @override void initState() { + _allValues = [...widget.values]; + _allValues.sort((value1, value2) => + widget.labelBuilder(value1).compareTo(widget.labelBuilder(value2))); _newSelectedValues = [...widget.selectedValues]; super.initState(); } @override Widget build(BuildContext context) { + final filteredValues = _allValues.where((v) => widget + .labelBuilder(v) + .toLowerCase() + .contains(_searchQuery.toLowerCase())); + return AlertDialog( title: Text(widget.title), - content: SizedBox( - width: 300, + content: DialogWidth( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + SearchField( + onChanged: (value) { + setState(() { + _searchQuery = value; + }); + }, + ), + SizedBox(height: 5), TextButton( onPressed: () => setState(() { _newSelectedValues.clear(); }), - child: Text('Clear'), + child: Text('Clear Selection'), ), Divider(), Expanded( child: ListView( shrinkWrap: true, - children: [ - ...widget.values - .map((value) => ListTile( - title: Text(widget.labelBuilder == null - ? value - : widget.labelBuilder!(value)), + children: filteredValues + .map((value) => Material( + child: ListTile( + title: Text(widget.labelBuilder(value)), leading: widget.leadingBuilder == null ? null : widget.leadingBuilder!(value), @@ -79,11 +100,15 @@ class _ValueSelectionDialogState extends State { } }); }, - )) - .toList(), - ], + ), + )) + .toList(), ), ), + if (widget.trailing != null) ...[ + SizedBox(height: 10), + widget.trailing!, + ] ], ), ), diff --git a/lib/widgets/inputs/borrower_input.dart b/lib/widgets/inputs/borrower_input.dart index 668a51e..ca631b6 100644 --- a/lib/widgets/inputs/borrower_input.dart +++ b/lib/widgets/inputs/borrower_input.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:spare_parts/entities/custom_user.dart'; -import 'package:spare_parts/widgets/inputs/user_selection_dialog.dart'; +import 'package:spare_parts/widgets/dialogs/user_selection_dialog.dart'; import 'package:spare_parts/widgets/user_avatar.dart'; class BorrowerInput extends StatelessWidget { diff --git a/lib/widgets/inputs/multiselect_button.dart b/lib/widgets/inputs/multiselect_button.dart index 139bb7a..d9b935a 100644 --- a/lib/widgets/inputs/multiselect_button.dart +++ b/lib/widgets/inputs/multiselect_button.dart @@ -1,37 +1,22 @@ import 'package:flutter/material.dart'; -import 'package:spare_parts/widgets/inputs/value_selection_dialog.dart'; -class MultiselectButton extends StatelessWidget { - final List values; - final List selectedValues; +class MultiselectButton extends StatelessWidget { + final bool hasSelection; final String buttonLabel; - final void Function(List) onConfirm; - final Widget Function(String value)? leadingBuilder; - final String Function(String value)? labelBuilder; - final IconData? icon; + final void Function(List) onConfirm; + final Widget dialog; const MultiselectButton({ super.key, - required this.selectedValues, + required this.hasSelection, required this.onConfirm, required this.buttonLabel, - required this.values, - this.leadingBuilder, - this.labelBuilder, - this.icon, + required this.dialog, }); void _handleChangeSelection(BuildContext context) async { - final newSelectedValues = await showDialog?>( - context: context, - builder: (context) => ValueSelectionDialog( - title: 'Pick $buttonLabel', - values: values, - selectedValues: selectedValues, - leadingBuilder: leadingBuilder, - labelBuilder: labelBuilder, - ), - ); + final newSelectedValues = await showDialog?>( + context: context, builder: (context) => dialog); if (newSelectedValues != null) { onConfirm(newSelectedValues); @@ -41,21 +26,13 @@ class MultiselectButton extends StatelessWidget { @override Widget build(BuildContext context) { final buttonStyle = TextButton.styleFrom( - foregroundColor: selectedValues.isEmpty - ? Theme.of(context).textTheme.bodyLarge!.color - : null, + foregroundColor: + hasSelection ? Theme.of(context).textTheme.bodyLarge!.color : null, ); - if (icon == null) { - return TextButton( - style: buttonStyle, - onPressed: () => _handleChangeSelection(context), - child: Text(buttonLabel), - ); - } return TextButton.icon( label: Text(buttonLabel), - icon: Icon(icon), + icon: Icon(Icons.filter_list), style: buttonStyle, onPressed: () => _handleChangeSelection(context), ); diff --git a/lib/widgets/inputs/search_field.dart b/lib/widgets/inputs/search_field.dart new file mode 100644 index 0000000..15054a8 --- /dev/null +++ b/lib/widgets/inputs/search_field.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; + +class SearchField extends StatefulWidget { + final void Function(String)? onChanged; + + const SearchField({ + super.key, + required this.onChanged, + }); + + @override + State createState() => _SearchFieldState(); +} + +class _SearchFieldState extends State { + final _searchFieldController = TextEditingController(); + IconData _suffixIcon = Icons.search; + + @override + void initState() { + _searchFieldController.addListener(() { + widget.onChanged?.call(_searchFieldController.text); + setState(() { + _suffixIcon = + _searchFieldController.text.isEmpty ? Icons.search : Icons.clear; + }); + }); + + super.initState(); + } + + @override + void dispose() { + _searchFieldController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return TextField( + key: Key('search'), + controller: _searchFieldController, + decoration: InputDecoration( + hintText: 'Search', + isDense: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(100)), + ), + suffixIcon: IconButton( + icon: Icon(_suffixIcon), + onPressed: _searchFieldController.clear, + ), + ), + enabled: widget.onChanged != null, + ); + } +} + +void main() { + runApp( + MaterialApp( + title: 'SearchField', + debugShowCheckedModeBanner: false, + home: Scaffold( + body: SearchField( + onChanged: (value) => print('New search value: $value'), + ), + ), + ), + ); +} diff --git a/lib/widgets/inputs/user_selection_dialog.dart b/lib/widgets/inputs/user_selection_dialog.dart deleted file mode 100644 index 67ac800..0000000 --- a/lib/widgets/inputs/user_selection_dialog.dart +++ /dev/null @@ -1,121 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:spare_parts/entities/custom_user.dart'; -import 'package:spare_parts/services/repositories/user_repository.dart'; -import 'package:spare_parts/widgets/inputs/new_user_input.dart'; -import 'package:spare_parts/widgets/user_avatar.dart'; - -class UserSelectionDialog extends StatefulWidget { - final List selectedUsers; - final List disabledUids; - final String title; - final bool isSingleSelection; - - const UserSelectionDialog({ - super.key, - required this.selectedUsers, - required this.title, - this.isSingleSelection = false, - this.disabledUids = const [], - }); - - @override - State createState() => _UserSelectionDialogState(); -} - -class _UserSelectionDialogState extends State { - late final List _newSelectedUsers; - - @override - void initState() { - _newSelectedUsers = [...widget.selectedUsers]; - super.initState(); - } - - UserRepository get userRepository => context.read(); - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: Text(widget.title), - content: SizedBox( - width: 300, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - TextButton( - onPressed: () => setState(() { - _newSelectedUsers.clear(); - }), - child: Text('Clear'), - ), - Divider(), - Flexible( - child: StreamBuilder>( - stream: userRepository.getAllStream(), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [CircularProgressIndicator()], - ); - } - - final users = snapshot.data!; - - return ListView.builder( - shrinkWrap: true, - itemCount: users.length, - itemBuilder: (context, index) { - final user = users[index]; - - return ListTile( - title: Text(user.name ?? ''), - leading: UserAvatar(photoUrl: user.photoURL), - selected: - _newSelectedUsers.any((u) => user.uid == u.uid), - selectedTileColor: - Theme.of(context).colorScheme.primary, - selectedColor: Theme.of(context).colorScheme.onPrimary, - enabled: !widget.disabledUids.contains(user.uid), - onTap: () { - setState(() { - if (_newSelectedUsers - .any((u) => user.uid == u.uid)) { - _newSelectedUsers - .removeWhere((u) => user.uid == u.uid); - } else { - if (widget.isSingleSelection) { - _newSelectedUsers.clear(); - } - _newSelectedUsers.add(user); - } - }); - }, - ); - }, - ); - }, - ), - ), - SizedBox(height: 10), - NewUserInput(), - ], - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, null), - child: Text('Cancel'), - ), - ElevatedButton( - onPressed: () { - Navigator.pop(context, _newSelectedUsers); - }, - child: Text('Select'), - ) - ], - ); - } -} diff --git a/test/pages/home_page/inventory_view/inventory_view_test.dart b/test/pages/home_page/inventory_view/inventory_view_test.dart index acfc953..a6c667c 100644 --- a/test/pages/home_page/inventory_view/inventory_view_test.dart +++ b/test/pages/home_page/inventory_view/inventory_view_test.dart @@ -1,18 +1,15 @@ import 'package:fake_cloud_firestore/fake_cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:spare_parts/dtos/user_dto.dart'; import 'package:spare_parts/entities/custom_user.dart'; import 'package:spare_parts/entities/inventory_item.dart'; import 'package:spare_parts/factories/inventory_item_factory.dart'; import 'package:spare_parts/pages/home_page/home_page.dart'; import 'package:spare_parts/pages/home_page/inventory_view/inventory_view.dart'; import 'package:spare_parts/pages/item_page/item_page.dart'; -import 'package:spare_parts/services/callable_service.mocks.dart'; import 'package:spare_parts/utilities/constants.dart'; -import 'package:spare_parts/widgets/inputs/user_selection_dialog.dart'; -import 'package:spare_parts/widgets/inputs/value_selection_dialog.dart'; +import 'package:spare_parts/widgets/dialogs/user_selection_dialog.dart'; +import 'package:spare_parts/widgets/inputs/new_user_input.dart'; import 'package:spare_parts/widgets/inventory_list_item.dart'; import 'package:spare_parts/widgets/inventory_list_item/inventory_item_form.dart'; @@ -573,7 +570,7 @@ void main() { var customName = 'Jane Doe'; var newUserNameInput = find.descendant( - of: find.byType(UserSelectionDialog), + of: find.byType(NewUserInput), matching: find.byType(TextField), ); await tester.enterText(newUserNameInput, customName); @@ -659,52 +656,36 @@ void main() { final user1 = CustomUser(uid: 'first', name: 'First'); final user2 = CustomUser(uid: 'second', name: 'Second'); - final callableService = MockCallableService(); - when(callableService.getUsers()).thenAnswer((_) => Future.value( - [user1, user2].map(UserDto.fromCustomUser).toList(), - )); + firestore.collection('users').doc(user1.uid).set(user1.toFirestore()); + firestore.collection('users').doc(user2.uid).set(user2.toFirestore()); - final deskItem = InventoryItem( - id: 'Desk#123', - type: 'Desk', - borrower: user1, - ); - final monitorItem = InventoryItem( - id: 'Monitor#123', - type: 'Monitor', - borrower: user2, - ); - await firestore - .collection('items') - .doc(deskItem.id) - .set(deskItem.toFirestore()); - await firestore - .collection('items') - .doc(monitorItem.id) - .set(monitorItem.toFirestore()); + final item1 = InventoryItemFactory().create(borrower: user1); + final item2 = InventoryItemFactory().create(borrower: user2); + + saveItemToFirestore(item1); + saveItemToFirestore(item2); await pumpPage( Scaffold(body: InventoryView()), tester, userRole: UserRole.admin, firestore: firestore, - callableService: callableService, ); await tester.tap(find.text('Borrowers')); await tester.pumpAndSettle(); await tester.tap(find.descendant( - of: find.byType(ValueSelectionDialog), + of: find.byType(AlertDialog), matching: find.text(user1.name!), )); await tester.tap(find.text('Select')); await tester.pumpAndSettle(); - expect(find.text(chairItem.id), findsNothing); - expect(find.text(deskItem.id), findsOneWidget); - expect(find.text(monitorItem.id), findsNothing); + expect(find.text(chairItem.name), findsNothing); + expect(find.text(item1.name), findsOneWidget); + expect(find.text(item2.name), findsNothing); }, ); }); @@ -773,256 +754,6 @@ void main() { }); }); - group('Searching for items', () { - testWidgets( - 'should return items with ids containing the query if name is not provided', - (WidgetTester tester) async { - final deskItem = InventoryItem(id: 'Desk#145', type: 'Desk'); - final monitorItem = InventoryItem(id: 'Monitor#999', type: 'Monitor'); - await firestore - .collection('items') - .doc(deskItem.id) - .set(deskItem.toFirestore()); - await firestore - .collection('items') - .doc(monitorItem.id) - .set(monitorItem.toFirestore()); - - await pumpPage( - Scaffold(body: InventoryView()), - tester, - userRole: UserRole.user, - firestore: firestore, - ); - - final searchField = find.byType(TextField); - await tester.enterText(searchField, '#'); - await tester.pumpAndSettle(); - - var listItems = find.byType(InventoryListItem); - expect(listItems, findsNWidgets(2)); - - await tester.enterText(searchField, '#1'); - await tester.pumpAndSettle(); - - listItems = find.byType(InventoryListItem); - expect(listItems, findsNWidgets(1)); - - await tester.enterText(searchField, '#14'); - await tester.pumpAndSettle(); - - listItems = find.byType(InventoryListItem); - expect(listItems, findsNWidgets(1)); - }); - - testWidgets('should return items with names containing the query', - (WidgetTester tester) async { - final deskItem = InventoryItem( - id: 'Desk#145', - name: 'Best Desk', - type: 'Desk', - ); - final monitorItem = InventoryItem( - id: 'Monitor#999', - name: 'Better Monitor', - type: 'Monitor', - ); - await firestore - .collection('items') - .doc(deskItem.id) - .set(deskItem.toFirestore()); - await firestore - .collection('items') - .doc(monitorItem.id) - .set(monitorItem.toFirestore()); - - await pumpPage( - Scaffold(body: InventoryView()), - tester, - userRole: UserRole.user, - firestore: firestore, - ); - - final searchField = find.byType(TextField); - await tester.enterText(searchField, 'Be'); - await tester.pumpAndSettle(); - - var listItems = find.byType(InventoryListItem); - expect(listItems, findsNWidgets(2)); - - await tester.enterText(searchField, 'Best'); - await tester.pumpAndSettle(); - - listItems = find.byType(InventoryListItem); - expect(listItems, findsNWidgets(1)); - }); - - testWidgets('should return items with borrowers containing the query', - (WidgetTester tester) async { - final deskItem = InventoryItem( - id: 'Desk#145', - name: 'Best Desk', - type: 'Desk', - borrower: CustomUser(uid: 'foo', name: 'John Doe'), - ); - final monitorItem = InventoryItem( - id: 'Monitor#999', - name: 'Better Monitor', - type: 'Monitor', - borrower: CustomUser(uid: 'bar', name: 'Jane Doe'), - ); - await firestore - .collection('items') - .doc(deskItem.id) - .set(deskItem.toFirestore()); - await firestore - .collection('items') - .doc(monitorItem.id) - .set(monitorItem.toFirestore()); - - await pumpPage( - Scaffold(body: InventoryView()), - tester, - userRole: UserRole.admin, - firestore: firestore, - ); - - final searchField = find.byType(TextField); - await tester.enterText(searchField, 'J'); - await tester.pumpAndSettle(); - - var listItems = find.byType(InventoryListItem); - expect(listItems, findsNWidgets(2)); - - await tester.enterText(searchField, 'John'); - await tester.pumpAndSettle(); - - listItems = find.byType(InventoryListItem); - expect(listItems, findsNWidgets(1)); - }); - - testWidgets('should display a clear button if query is not empty', - (WidgetTester tester) async { - await pumpPage( - Scaffold(body: InventoryView()), - tester, - userRole: UserRole.user, - firestore: firestore, - ); - - final searchButton = find.byIcon(Icons.search); - expect(searchButton, findsOneWidget); - - final searchField = find.byType(TextField); - await tester.enterText(searchField, '#'); - await tester.pumpAndSettle(); - - final clearButton = find.byIcon(Icons.clear); - expect(clearButton, findsOneWidget); - }); - - testWidgets('should clear search query when clear button tapped', - (WidgetTester tester) async { - await pumpPage( - Scaffold(body: InventoryView()), - tester, - userRole: UserRole.user, - firestore: firestore, - ); - - const query = '#'; - - final searchField = find.byType(TextField); - await tester.enterText(searchField, query); - await tester.pumpAndSettle(); - - final clearButton = find.byIcon(Icons.clear); - await tester.tap(clearButton); - await tester.pumpAndSettle(); - - expect(find.text(query), findsNothing); - }); - - testWidgets('should render all inventory items when clear button tapped', - (WidgetTester tester) async { - await pumpPage( - Scaffold(body: InventoryView()), - tester, - userRole: UserRole.user, - firestore: firestore, - ); - - const query = '???'; - - final searchField = find.byType(TextField); - await tester.enterText(searchField, query); - await tester.pumpAndSettle(); - - var listItems = find.byType(InventoryListItem); - expect(listItems, findsNothing); - - final clearButton = find.byIcon(Icons.clear); - await tester.tap(clearButton); - await tester.pumpAndSettle(); - - listItems = find.byType(InventoryListItem); - expect(listItems, findsOneWidget); - }); - - testWidgets('should be case insensitive', (WidgetTester tester) async { - await pumpPage( - Scaffold(body: InventoryView()), - tester, - userRole: UserRole.user, - firestore: firestore, - ); - - var query = chairItem.name.toLowerCase(); - final searchField = find.byType(TextField); - await tester.enterText(searchField, query); - await tester.pumpAndSettle(); - - var listItems = find.byType(InventoryListItem); - expect(listItems, findsOneWidget); - - query = chairItem.name.toUpperCase(); - await tester.enterText(searchField, query); - await tester.pumpAndSettle(); - - listItems = find.byType(InventoryListItem); - expect(listItems, findsOneWidget); - }); - }); - - group('Selecting items', () { - testWidgets('disables search', (WidgetTester tester) async { - final deskItem = InventoryItem(id: 'Desk#145', type: 'Desk'); - await firestore - .collection('items') - .doc(deskItem.id) - .set(deskItem.toFirestore()); - - await pumpPage( - Scaffold(body: InventoryView()), - tester, - userRole: UserRole.admin, - firestore: firestore, - ); - - final searchField = find.byKey(Key('search')); - - final deskListTile = find.ancestor( - of: find.text(deskItem.name), - matching: find.byType(ListTile), - ); - - await tester.longPress(deskListTile); - await tester.pumpAndSettle(); - - expect(tester.widget(searchField).enabled, isFalse); - }); - }); - group('Loading more items', () { testWidgets('loads more items', (tester) async { tester.binding.setSurfaceSize(Size(1000, 3000)); diff --git a/test/pages/home_page/settings_view/borrowing_rules_setting.dart b/test/pages/home_page/settings_view/borrowing_rules_setting_test.dart similarity index 96% rename from test/pages/home_page/settings_view/borrowing_rules_setting.dart rename to test/pages/home_page/settings_view/borrowing_rules_setting_test.dart index faf88df..ca8356a 100644 --- a/test/pages/home_page/settings_view/borrowing_rules_setting.dart +++ b/test/pages/home_page/settings_view/borrowing_rules_setting_test.dart @@ -4,7 +4,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:spare_parts/entities/borrowing_rule.dart'; import 'package:spare_parts/pages/home_page/settings_view/borrowing_rules_setting/borrowing_rules_setting.dart'; import 'package:spare_parts/utilities/constants.dart'; -import 'package:spare_parts/widgets/inputs/value_selection_dialog.dart'; import '../../../helpers/test_helpers.dart'; @@ -87,9 +86,9 @@ void main() { ); testWidgets( - 'Shows a delete button if the borrowing limit is 1', + 'Shows a delete button if the borrowing limit is 0', (WidgetTester tester) async { - final borrowingRule = BorrowingRule(type: 'Desk', maxBorrowingCount: 2); + final borrowingRule = BorrowingRule(type: 'Desk', maxBorrowingCount: 1); await firestore .collection('borrowingRules') .add(borrowingRule.toFirestore()); @@ -159,7 +158,7 @@ void main() { 'Can delete a borrowing rule', (WidgetTester tester) async { final borrowingRules = [ - BorrowingRule(type: 'Desk', maxBorrowingCount: 1), + BorrowingRule(type: 'Desk', maxBorrowingCount: 0), BorrowingRule(type: 'Chair', maxBorrowingCount: 3), ]; for (final rule in borrowingRules) { @@ -196,7 +195,7 @@ void main() { await tester.tap(find.byIcon(Icons.edit).first); await tester.pumpAndSettle(); - const newType = 'Monitor'; + const newType = 'Chair'; await tester.tap(find.text(newType)); await tester.tap(find.text('Select')); await tester.pumpAndSettle(); @@ -233,7 +232,7 @@ void main() { final existingType = secondBorrowingRule.type; final existingTypeOption = find.descendant( - of: find.byType(ValueSelectionDialog), + of: find.byType(AlertDialog), matching: find.text(existingType), ); await tester.tap(existingTypeOption); @@ -244,4 +243,4 @@ void main() { }, ); }); -} \ No newline at end of file +} diff --git a/test/widgets/inputs/search_field_test.dart b/test/widgets/inputs/search_field_test.dart new file mode 100644 index 0000000..c894a97 --- /dev/null +++ b/test/widgets/inputs/search_field_test.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spare_parts/utilities/constants.dart'; +import 'package:spare_parts/widgets/inputs/search_field.dart'; + +import '../../helpers/test_helpers.dart'; + +void main() { + group('SearchField', () { + testWidgets('should display a clear button if query is not empty', + (WidgetTester tester) async { + await pumpPage( + Scaffold(body: SearchField(onChanged: (_) {})), + tester, + userRole: UserRole.user, + ); + + final searchButton = find.byIcon(Icons.search); + expect(searchButton, findsOneWidget); + + final searchField = find.byType(TextField); + await tester.enterText(searchField, '#'); + await tester.pumpAndSettle(); + + final clearButton = find.byIcon(Icons.clear); + expect(clearButton, findsOneWidget); + }); + + testWidgets('should clear search query when clear button tapped', + (WidgetTester tester) async { + await pumpPage( + Scaffold(body: SearchField(onChanged: (_) {})), + tester, + userRole: UserRole.user, + ); + + const query = '#'; + + final searchField = find.byType(TextField); + await tester.enterText(searchField, query); + await tester.pumpAndSettle(); + + final clearButton = find.byIcon(Icons.clear); + await tester.tap(clearButton); + await tester.pumpAndSettle(); + + expect(find.text(query), findsNothing); + }); + }); +} diff --git a/test/widgets/inputs/value_selection_dialog_test.dart b/test/widgets/inputs/value_selection_dialog_test.dart new file mode 100644 index 0000000..6dc5e67 --- /dev/null +++ b/test/widgets/inputs/value_selection_dialog_test.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spare_parts/widgets/inputs/search_field.dart'; +import 'package:spare_parts/widgets/dialogs/value_selection_dialog.dart'; + +import '../../helpers/test_helpers.dart'; + +void main() { + group('ValueSelectionDialog', () { + testWidgets('can be searched by label', (WidgetTester tester) async { + const firstValue = 'asd'; + const secondValue = 'qwe'; + + await pumpPage( + ValueSelectionDialog( + selectedValues: [], + title: 'Select value', + values: [firstValue, secondValue], + labelBuilder: (value) => value, + ), + tester, + ); + + Finder firstOptionListItem = find.descendant( + of: find.byType(ListTile), + matching: find.text(firstValue), + ); + expect(firstOptionListItem, findsOneWidget); + Finder secondOptionListItem = find.descendant( + of: find.byType(ListTile), + matching: find.text(secondValue), + ); + expect(secondOptionListItem, findsOneWidget); + + final searchInput = find.descendant( + of: find.byType(SearchField), + matching: find.byType(TextField), + ); + await tester.enterText(searchInput, 'a'); + await tester.pumpAndSettle(); + + firstOptionListItem = find.descendant( + of: find.byType(ListTile), + matching: find.text(firstValue), + ); + expect(firstOptionListItem, findsOneWidget); + secondOptionListItem = find.descendant( + of: find.byType(ListTile), + matching: find.text(secondValue), + ); + expect(secondOptionListItem, findsNothing); + }); + }); +}