From cb5f23770c135ca376a400215f181373bbbf6bc5 Mon Sep 17 00:00:00 2001 From: Dillon Fagan Date: Thu, 26 Sep 2024 14:34:43 -0400 Subject: [PATCH 1/4] WIP Add item search button --- .../layouts/inventory_desktop_layout.dart | 21 +++------ .../details/inventory_details_pane.dart | 9 +--- .../modules/things/search/search_field.dart | 44 +++++++++++++++++++ .../lib/widgets/fields/search_field.dart | 34 ++++++++------ .../lib/widgets/panes/header_divider.dart | 16 +++++++ 5 files changed, 88 insertions(+), 36 deletions(-) create mode 100644 apps/librarian/lib/modules/things/search/search_field.dart create mode 100644 apps/librarian/lib/widgets/panes/header_divider.dart diff --git a/apps/librarian/lib/dashboard/layouts/inventory_desktop_layout.dart b/apps/librarian/lib/dashboard/layouts/inventory_desktop_layout.dart index 8d137c6..567faf7 100644 --- a/apps/librarian/lib/dashboard/layouts/inventory_desktop_layout.dart +++ b/apps/librarian/lib/dashboard/layouts/inventory_desktop_layout.dart @@ -1,10 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:librarian_app/modules/things/search/search_field.dart'; import 'package:librarian_app/widgets/panes/list_pane.dart'; import 'package:librarian_app/widgets/panes/pane_header.dart'; -import 'package:librarian_app/widgets/fields/search_field.dart'; -import 'package:librarian_app/modules/things/providers/selected_thing_provider.dart'; -import 'package:librarian_app/modules/things/providers/things_filter_provider.dart'; import '../../modules/things/details/inventory_details_pane.dart'; import '../../modules/things/details/inventory/inventory_list/inventory_list_view.dart'; @@ -14,24 +12,15 @@ class InventoryDesktopLayout extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return Row( + return const Row( children: [ ListPane( header: PaneHeader( - child: SearchField( - text: ref.watch(thingsFilterProvider), - onChanged: (value) { - ref.read(thingsFilterProvider.notifier).state = value; - }, - onClearPressed: () { - ref.read(thingsFilterProvider.notifier).state = null; - ref.read(selectedThingProvider.notifier).state = null; - }, - ), + child: ThingsSearchField(), ), - child: const InventoryListView(), + child: InventoryListView(), ), - const Expanded( + Expanded( child: InventoryDetailsPane(), ), ], diff --git a/apps/librarian/lib/modules/things/details/inventory_details_pane.dart b/apps/librarian/lib/modules/things/details/inventory_details_pane.dart index 57d9fc4..1d87447 100644 --- a/apps/librarian/lib/modules/things/details/inventory_details_pane.dart +++ b/apps/librarian/lib/modules/things/details/inventory_details_pane.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:librarian_app/modules/things/providers/thing_details_controller_provider.dart'; import 'package:librarian_app/widgets/dialogs/save_dialog.dart'; +import 'package:librarian_app/widgets/panes/header_divider.dart'; import 'package:librarian_app/widgets/panes/pane_header.dart'; import 'package:librarian_app/core/api/models/detailed_thing_model.dart'; import 'package:librarian_app/modules/things/providers/edited_thing_details_providers.dart'; @@ -92,13 +93,7 @@ class InventoryDetailsPane extends ConsumerWidget { icon: const Icon(Icons.cancel), tooltip: 'Discard Changes', ), - SizedBox( - height: 24, - width: 24, - child: VerticalDivider( - color: Colors.white.withOpacity(0.3), - ), - ), + const HeaderDivider(), IconButton( onPressed: delete, icon: const Icon(Icons.delete_forever), diff --git a/apps/librarian/lib/modules/things/search/search_field.dart b/apps/librarian/lib/modules/things/search/search_field.dart new file mode 100644 index 0000000..9146a8f --- /dev/null +++ b/apps/librarian/lib/modules/things/search/search_field.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:librarian_app/widgets/fields/search_field.dart'; + +import '../providers/selected_thing_provider.dart'; +import '../providers/things_filter_provider.dart'; + +class ThingsSearchField extends ConsumerWidget { + const ThingsSearchField({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SearchField( + text: ref.watch(thingsFilterProvider), + onChanged: (value) { + ref.read(thingsFilterProvider.notifier).state = value; + }, + onClearPressed: () { + ref.read(thingsFilterProvider.notifier).state = null; + ref.read(selectedThingProvider.notifier).state = null; + }, + trailing: const _ItemSearchButton(), + ); + } +} + +class _ItemSearchButton extends StatelessWidget { + const _ItemSearchButton(); + + @override + Widget build(BuildContext context) { + return IconButton( + onPressed: () { + showDialog( + context: context, + builder: (context) => const Dialog( + child: Text('Enter Item Number'), + ), + ); + }, + icon: const Icon(Icons.numbers), + ); + } +} diff --git a/apps/librarian/lib/widgets/fields/search_field.dart b/apps/librarian/lib/widgets/fields/search_field.dart index 50b3bf9..d65e009 100644 --- a/apps/librarian/lib/widgets/fields/search_field.dart +++ b/apps/librarian/lib/widgets/fields/search_field.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:librarian_app/widgets/panes/header_divider.dart'; class SearchField extends StatefulWidget { const SearchField({ @@ -6,8 +7,10 @@ class SearchField extends StatefulWidget { required this.onChanged, this.onClearPressed, this.text, + this.trailing, }); + final Widget? trailing; final void Function(String value) onChanged; final void Function()? onClearPressed; final String? text; @@ -22,20 +25,21 @@ class _SearchFieldState extends State { @override Widget build(BuildContext context) { return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - TextField( - controller: _searchController, - onChanged: widget.onChanged, - decoration: InputDecoration( - border: InputBorder.none, - constraints: const BoxConstraints(maxWidth: 400), - hintText: 'Search...', - icon: Icon( - Icons.search_rounded, - color: _searchController.text.isEmpty - ? null - : Theme.of(context).primaryIconTheme.color, + Expanded( + child: TextField( + controller: _searchController, + onChanged: widget.onChanged, + decoration: InputDecoration( + border: InputBorder.none, + constraints: const BoxConstraints(maxWidth: 400), + hintText: 'Search...', + icon: Icon( + Icons.search_rounded, + color: _searchController.text.isEmpty + ? null + : Theme.of(context).primaryIconTheme.color, + ), ), ), ), @@ -49,6 +53,10 @@ class _SearchFieldState extends State { icon: const Icon(Icons.clear_rounded), tooltip: 'Clear', ), + if (widget.trailing != null) ...[ + const HeaderDivider(), + widget.trailing!, + ], ], ); } diff --git a/apps/librarian/lib/widgets/panes/header_divider.dart b/apps/librarian/lib/widgets/panes/header_divider.dart new file mode 100644 index 0000000..b507100 --- /dev/null +++ b/apps/librarian/lib/widgets/panes/header_divider.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +class HeaderDivider extends StatelessWidget { + const HeaderDivider({super.key}); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 24, + width: 24, + child: VerticalDivider( + color: Colors.white.withOpacity(0.3), + ), + ); + } +} From 8f9653622699a64e16c34b4b4a183ff0c7988786 Mon Sep 17 00:00:00 2001 From: Dillon Fagan Date: Thu, 26 Sep 2024 16:58:18 -0400 Subject: [PATCH 2/4] Implement Item Lookup --- .../things/search/item_lookup_button.dart | 115 ++++++++++++++++++ .../modules/things/search/search_field.dart | 25 +--- 2 files changed, 120 insertions(+), 20 deletions(-) create mode 100644 apps/librarian/lib/modules/things/search/item_lookup_button.dart diff --git a/apps/librarian/lib/modules/things/search/item_lookup_button.dart b/apps/librarian/lib/modules/things/search/item_lookup_button.dart new file mode 100644 index 0000000..cf57f8d --- /dev/null +++ b/apps/librarian/lib/modules/things/search/item_lookup_button.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:librarian_app/modules/things/providers/find_things.dart'; +import 'package:librarian_app/modules/things/providers/selected_thing_provider.dart'; +import 'package:librarian_app/widgets/input_decoration.dart'; + +class ItemLookupButton extends StatelessWidget { + const ItemLookupButton({super.key}); + + @override + Widget build(BuildContext context) { + return IconButton( + onPressed: () { + showDialog( + context: context, + builder: (context) => const ItemLookupDialog(), + ); + }, + icon: const Icon(Icons.numbers), + ); + } +} + +class ItemLookupDialog extends ConsumerStatefulWidget { + const ItemLookupDialog({super.key}); + + @override + ConsumerState createState() { + return _ItemLookupDialogState(); + } +} + +class _ItemLookupDialogState extends ConsumerState { + final formKey = GlobalKey(); + final numberController = TextEditingController(); + + String? errorMessage; + + void onSubmit() { + if (formKey.currentState!.validate()) { + search(int.parse(numberController.text)); + } + } + + void search(int number) async { + final things = await ref.read(findThingsByItem(number: number)); + + if (things.isEmpty) { + setState(() { + errorMessage = 'Item #$number does not exist'; + }); + return; + } + + ref.read(selectedThingProvider.notifier).state = things[0]; + Future.delayed(Duration.zero, () { + Navigator.of(context).pop(); + }); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + icon: const Icon(Icons.search), + title: const Text('Item Lookup'), + actions: [ + OutlinedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + ValueListenableBuilder( + valueListenable: numberController, + builder: (context, name, child) => FilledButton( + onPressed: name.text.isNotEmpty ? onSubmit : null, + child: const Text('Look up'), + ), + ), + ], + contentPadding: const EdgeInsets.all(16), + content: SizedBox( + width: 500, + child: Form( + key: formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: TextFormField( + controller: numberController, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Number is required'; + } + + return null; + }, + onChanged: (value) { + if (value.length < 3) { + return; + } + }, + onFieldSubmitted: (value) => onSubmit(), + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: inputDecoration.copyWith( + labelText: 'Item Number', + constraints: const BoxConstraints(minWidth: 500), + prefixIcon: const Icon(Icons.numbers), + errorText: errorMessage, + ), + ), + ), + ), + ); + } +} diff --git a/apps/librarian/lib/modules/things/search/search_field.dart b/apps/librarian/lib/modules/things/search/search_field.dart index 9146a8f..f654ab2 100644 --- a/apps/librarian/lib/modules/things/search/search_field.dart +++ b/apps/librarian/lib/modules/things/search/search_field.dart @@ -4,6 +4,7 @@ import 'package:librarian_app/widgets/fields/search_field.dart'; import '../providers/selected_thing_provider.dart'; import '../providers/things_filter_provider.dart'; +import 'item_lookup_button.dart'; class ThingsSearchField extends ConsumerWidget { const ThingsSearchField({super.key}); @@ -19,26 +20,10 @@ class ThingsSearchField extends ConsumerWidget { ref.read(thingsFilterProvider.notifier).state = null; ref.read(selectedThingProvider.notifier).state = null; }, - trailing: const _ItemSearchButton(), - ); - } -} - -class _ItemSearchButton extends StatelessWidget { - const _ItemSearchButton(); - - @override - Widget build(BuildContext context) { - return IconButton( - onPressed: () { - showDialog( - context: context, - builder: (context) => const Dialog( - child: Text('Enter Item Number'), - ), - ); - }, - icon: const Icon(Icons.numbers), + trailing: const Tooltip( + message: 'Item Lookup', + child: ItemLookupButton(), + ), ); } } From 89d1c6fd142a126bcfe0c615cfcb755e2d041a3a Mon Sep 17 00:00:00 2001 From: Dillon Fagan Date: Thu, 26 Sep 2024 17:50:18 -0400 Subject: [PATCH 3/4] Open Item Details Drawer --- .../things/details/inventory_details.dart | 40 ++------------ .../providers/item_details_orchestrator.dart | 53 +++++++++++++++++++ .../things/search/item_lookup_button.dart | 46 ++++++++++++---- 3 files changed, 93 insertions(+), 46 deletions(-) create mode 100644 apps/librarian/lib/modules/things/providers/item_details_orchestrator.dart diff --git a/apps/librarian/lib/modules/things/details/inventory_details.dart b/apps/librarian/lib/modules/things/details/inventory_details.dart index 5dc6699..0941413 100644 --- a/apps/librarian/lib/modules/things/details/inventory_details.dart +++ b/apps/librarian/lib/modules/things/details/inventory_details.dart @@ -2,22 +2,18 @@ import 'package:file_picker/_internal/file_picker_web.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:librarian_app/dashboard/providers/end_drawer_provider.dart'; import 'package:librarian_app/modules/things/details/inventory/create_items/create_items_dialog.dart'; -import 'package:librarian_app/modules/things/details/inventory/item_details/drawer.dart'; import 'package:librarian_app/modules/things/details/thing_details/thing_details_card.dart'; import 'package:librarian_app/core/api/models/updated_image_model.dart'; -import 'package:librarian_app/modules/things/details/inventory/item_details_page.dart'; import 'package:librarian_app/modules/things/providers/edited_thing_details_providers.dart'; +import 'package:librarian_app/modules/things/providers/item_details_orchestrator.dart'; import 'package:librarian_app/modules/things/providers/selected_thing_provider.dart'; import 'package:librarian_app/modules/things/providers/thing_details_provider.dart'; import 'package:librarian_app/modules/things/providers/things_repository_provider.dart'; import 'package:librarian_app/modules/things/details/categories/categories_card.dart'; import 'package:librarian_app/modules/things/details/inventory/items_card.dart'; import 'package:librarian_app/modules/things/details/image/thing_image_card.dart'; -import 'package:librarian_app/utils/media_query.dart'; -import 'inventory/item_details/item_details_controller.dart'; import 'linked_things/card.dart'; class InventoryDetails extends ConsumerWidget { @@ -82,37 +78,9 @@ class InventoryDetails extends ConsumerWidget { ItemsCard( items: details.items, availableItemsCount: details.available, - onTap: (item) { - if (isMobile(context)) { - Navigator.of(context) - .push(MaterialPageRoute(builder: (context) { - return ItemDetailsPage( - item: item, - hiddenLocked: details.hidden, - ); - })); - return; - } - - final detailsController = ItemDetailsController( - item: item, - repository: ref.read(thingsRepositoryProvider.notifier), - onSave: () { - // setState(() => _isLoading = true); - }, - onSaveComplete: () { - // setState(() => _isLoading = false); - }, - ); - - ref.read(endDrawerProvider).openEndDrawer( - context, - ItemDetailsDrawer( - controller: detailsController, - isHiddenLocked: details.hidden, - ), - ); - }, + onTap: (item) => ref + .read(itemDetailsOrchestrator) + .openItem(context, item: item, hiddenLocked: details.hidden), onAddItemsPressed: () { showDialog( context: context, diff --git a/apps/librarian/lib/modules/things/providers/item_details_orchestrator.dart b/apps/librarian/lib/modules/things/providers/item_details_orchestrator.dart new file mode 100644 index 0000000..23298f5 --- /dev/null +++ b/apps/librarian/lib/modules/things/providers/item_details_orchestrator.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:librarian_app/core/api/models/item_model.dart'; +import 'package:librarian_app/dashboard/providers/end_drawer_provider.dart'; +import 'package:librarian_app/utils/media_query.dart'; + +import '../details/inventory/item_details/drawer.dart'; +import '../details/inventory/item_details/item_details_controller.dart'; +import '../details/inventory/item_details_page.dart'; +import 'things_repository_provider.dart'; + +class ItemDetailsOrchestrator { + ItemDetailsOrchestrator(this.ref); + + final Ref ref; + + void openItem( + BuildContext context, { + required ItemModel item, + required bool hiddenLocked, + }) { + if (isMobile(context)) { + Navigator.of(context).push(MaterialPageRoute(builder: (context) { + return ItemDetailsPage( + item: item, + hiddenLocked: hiddenLocked, + ); + })); + return; + } + + final detailsController = ItemDetailsController( + item: item, + repository: ref.read(thingsRepositoryProvider.notifier), + onSave: () { + // setState(() => _isLoading = true); + }, + onSaveComplete: () { + // setState(() => _isLoading = false); + }, + ); + + ref.read(endDrawerProvider).openEndDrawer( + context, + ItemDetailsDrawer( + controller: detailsController, + isHiddenLocked: hiddenLocked, + ), + ); + } +} + +final itemDetailsOrchestrator = Provider((ref) => ItemDetailsOrchestrator(ref)); diff --git a/apps/librarian/lib/modules/things/search/item_lookup_button.dart b/apps/librarian/lib/modules/things/search/item_lookup_button.dart index cf57f8d..3ae85e3 100644 --- a/apps/librarian/lib/modules/things/search/item_lookup_button.dart +++ b/apps/librarian/lib/modules/things/search/item_lookup_button.dart @@ -1,21 +1,35 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:librarian_app/core/api/models/item_model.dart'; +import 'package:librarian_app/core/api/models/thing_model.dart'; import 'package:librarian_app/modules/things/providers/find_things.dart'; +import 'package:librarian_app/modules/things/providers/item_details_orchestrator.dart'; import 'package:librarian_app/modules/things/providers/selected_thing_provider.dart'; +import 'package:librarian_app/modules/things/providers/things_repository_provider.dart'; import 'package:librarian_app/widgets/input_decoration.dart'; -class ItemLookupButton extends StatelessWidget { +class ItemLookupButton extends ConsumerWidget { const ItemLookupButton({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return IconButton( - onPressed: () { - showDialog( + onPressed: () async { + final result = await showDialog( context: context, builder: (context) => const ItemLookupDialog(), ); + + // Load Thing and Item Details + if (result != null) { + ref.read(selectedThingProvider.notifier).state = result.thing; + + Future.delayed(Duration.zero, () { + ref.read(itemDetailsOrchestrator).openItem(context, + item: result.item, hiddenLocked: result.thing.hidden); + }); + } }, icon: const Icon(Icons.numbers), ); @@ -44,18 +58,22 @@ class _ItemLookupDialogState extends ConsumerState { } void search(int number) async { - final things = await ref.read(findThingsByItem(number: number)); + final item = await ref + .read(thingsRepositoryProvider.notifier) + .getItem(number: number); - if (things.isEmpty) { + if (item == null) { setState(() { errorMessage = 'Item #$number does not exist'; }); return; } - ref.read(selectedThingProvider.notifier).state = things[0]; - Future.delayed(Duration.zero, () { - Navigator.of(context).pop(); + final things = await ref.read(findThingsByItem(number: number)); + final thing = things[0]; + + await Future.delayed(Duration.zero, () { + Navigator.of(context).pop(LookupResult(item: item, thing: thing)); }); } @@ -67,7 +85,7 @@ class _ItemLookupDialogState extends ConsumerState { actions: [ OutlinedButton( onPressed: () { - Navigator.of(context).pop(); + Navigator.of(context).pop(null); }, child: const Text('Cancel'), ), @@ -86,6 +104,7 @@ class _ItemLookupDialogState extends ConsumerState { key: formKey, autovalidateMode: AutovalidateMode.onUserInteraction, child: TextFormField( + autofocus: true, controller: numberController, validator: (value) { if (value == null || value.isEmpty) { @@ -113,3 +132,10 @@ class _ItemLookupDialogState extends ConsumerState { ); } } + +class LookupResult { + final ItemModel item; + final ThingModel thing; + + LookupResult({required this.item, required this.thing}); +} From 51f18634bba319642399c90abe9255cf4b63ce7c Mon Sep 17 00:00:00 2001 From: Dillon Fagan Date: Thu, 26 Sep 2024 17:56:55 -0400 Subject: [PATCH 4/4] Bump librarian build to +18 --- apps/librarian/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/librarian/pubspec.yaml b/apps/librarian/pubspec.yaml index e3f48c2..0466fc3 100644 --- a/apps/librarian/pubspec.yaml +++ b/apps/librarian/pubspec.yaml @@ -10,7 +10,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # followed by an optional build number separated by a +. # Both the version and the builder number may be overridden in flutter # build by specifying --build-name and --build-number, respectively. -version: 1.0.0+17 +version: 1.0.0+18 environment: sdk: '>=3.0.0'