From d2ff909bf2b4fb5eb97cfad53ae8d2280c731318 Mon Sep 17 00:00:00 2001 From: hannathkadher Date: Wed, 5 Mar 2025 22:24:45 +0400 Subject: [PATCH] updated tag dialogue selection --- lib/common/tag_dialog_textfield_dropdown.dart | 185 ++++++++++++++++++ .../bloc/space_management_bloc.dart | 4 +- .../all_spaces/model/tag.dart | 5 +- .../bloc/assign_tag_model_bloc.dart | 75 ++++--- .../bloc/assign_tag_model_event.dart | 2 +- .../bloc/assign_tag_model_state.dart | 2 +- .../views/assign_tag_models_dialog.dart | 15 +- .../create_space_template_body_model.dart | 2 +- 8 files changed, 237 insertions(+), 53 deletions(-) create mode 100644 lib/common/tag_dialog_textfield_dropdown.dart diff --git a/lib/common/tag_dialog_textfield_dropdown.dart b/lib/common/tag_dialog_textfield_dropdown.dart new file mode 100644 index 00000000..219e03ce --- /dev/null +++ b/lib/common/tag_dialog_textfield_dropdown.dart @@ -0,0 +1,185 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class TagDialogTextfieldDropdown extends StatefulWidget { + final List items; + final ValueChanged onSelected; + final Tag? initialValue; + final String product; + + const TagDialogTextfieldDropdown({ + Key? key, + required this.items, + required this.onSelected, + this.initialValue, + required this.product, + }) : super(key: key); + + @override + _DialogTextfieldDropdownState createState() => _DialogTextfieldDropdownState(); +} + +class _DialogTextfieldDropdownState extends State { + bool _isOpen = false; + OverlayEntry? _overlayEntry; + final TextEditingController _controller = TextEditingController(); + final FocusNode _focusNode = FocusNode(); + List _filteredItems = []; + + @override + void initState() { + super.initState(); + _controller.text = widget.initialValue?.tag ?? ''; + + _filterItems(); + + _focusNode.addListener(() { + if (!_focusNode.hasFocus) { + _closeDropdown(); + } + }); + } + + void _filterItems() { + setState(() { + _filteredItems = widget.items.where((tag) => tag.product?.uuid == widget.product).toList(); + }); + } + + void _toggleDropdown() { + if (_isOpen) { + _closeDropdown(); + } else { + _openDropdown(); + } + } + + void _openDropdown() { + _overlayEntry = _createOverlayEntry(); + Overlay.of(context).insert(_overlayEntry!); + _isOpen = true; + } + + void _closeDropdown() { + if (_isOpen && _overlayEntry != null) { + _overlayEntry!.remove(); + _overlayEntry = null; + _isOpen = false; + } + } + + OverlayEntry _createOverlayEntry() { + final renderBox = context.findRenderObject() as RenderBox; + final size = renderBox.size; + final offset = renderBox.localToGlobal(Offset.zero); + + return OverlayEntry( + builder: (context) { + return GestureDetector( + onTap: _closeDropdown, + behavior: HitTestBehavior.translucent, + child: Stack( + children: [ + Positioned( + left: offset.dx, + top: offset.dy + size.height, + width: size.width, + child: Material( + elevation: 4.0, + child: Container( + color: ColorsManager.whiteColors, + constraints: const BoxConstraints(maxHeight: 200.0), + child: StatefulBuilder( + builder: (context, setStateDropdown) { + return ListView.builder( + shrinkWrap: true, + itemCount: _filteredItems.length, + itemBuilder: (context, index) { + final tag = _filteredItems[index]; + + return Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: ColorsManager.lightGrayBorderColor, + width: 1.0, + ), + ), + ), + child: ListTile( + title: Text(tag.tag ?? '', + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(color: ColorsManager.textPrimaryColor)), + onTap: () { + _controller.text = tag.tag ?? ''; + widget.onSelected(tag); + setState(() { + _filteredItems.remove(tag); + }); + _closeDropdown(); + }, + ), + ); + }, + ); + }, + ), + ), + ), + ), + ], + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + behavior: HitTestBehavior.opaque, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + decoration: BoxDecoration( + border: Border.all(color: ColorsManager.transparentColor), + borderRadius: BorderRadius.circular(8.0), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: TextFormField( + controller: _controller, + focusNode: _focusNode, + onFieldSubmitted: (value) { + final selectedTag = _filteredItems.firstWhere((tag) => tag.tag == value, + orElse: () => Tag(tag: value)); + widget.onSelected(selectedTag); + _closeDropdown(); + }, + onTapOutside: (event) { + widget.onSelected(_filteredItems.firstWhere((tag) => tag.tag == _controller.text, + orElse: () => Tag(tag: _controller.text))); + _closeDropdown(); + }, + style: Theme.of(context).textTheme.bodyMedium, + decoration: const InputDecoration( + hintText: 'Enter or Select a tag', + border: InputBorder.none, + ), + ), + ), + GestureDetector( + onTap: _toggleDropdown, + child: const Icon(Icons.arrow_drop_down), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart b/lib/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart index 1035634a..5dcd3e89 100644 --- a/lib/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart +++ b/lib/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart @@ -60,7 +60,7 @@ class SpaceManagementBloc extends Bloc { - final List allTags; +class AssignTagModelBloc extends Bloc { final List projectTags; - AssignTagModelBloc(this.allTags, this.projectTags) : super(AssignTagModelInitial()) { + AssignTagModelBloc(this.projectTags) : super(AssignTagModelInitial()) { on((event, emit) { final initialTags = event.initialTags ?? []; final existingTagCounts = {}; for (var tag in initialTags) { if (tag.product != null) { - existingTagCounts[tag.product!.uuid] = - (existingTagCounts[tag.product!.uuid] ?? 0) + 1; + existingTagCounts[tag.product!.uuid] = (existingTagCounts[tag.product!.uuid] ?? 0) + 1; } } @@ -25,17 +22,14 @@ class AssignTagModelBloc for (var selectedProduct in event.addedProducts) { final existingCount = existingTagCounts[selectedProduct.productId] ?? 0; - if (selectedProduct.count == 0 || - selectedProduct.count <= existingCount) { - tags.addAll(initialTags - .where((tag) => tag.product?.uuid == selectedProduct.productId)); + if (selectedProduct.count == 0 || selectedProduct.count <= existingCount) { + tags.addAll(initialTags.where((tag) => tag.product?.uuid == selectedProduct.productId)); continue; } final missingCount = selectedProduct.count - existingCount; - tags.addAll(initialTags - .where((tag) => tag.product?.uuid == selectedProduct.productId)); + tags.addAll(initialTags.where((tag) => tag.product?.uuid == selectedProduct.productId)); if (missingCount > 0) { tags.addAll(List.generate( @@ -49,7 +43,7 @@ class AssignTagModelBloc } } - final updatedTags = _calculateAvailableTags(allTags, tags); + final updatedTags = _calculateAvailableTags(projectTags, tags); emit(AssignTagModelLoaded( tags: tags, @@ -60,11 +54,20 @@ class AssignTagModelBloc on((event, emit) { final currentState = state; - if (currentState is AssignTagModelLoaded && - currentState.tags.isNotEmpty) { + if (currentState is AssignTagModelLoaded && currentState.tags.isNotEmpty) { final tags = List.from(currentState.tags); - tags[event.index] = tags[event.index].copyWith(tag: event.tag); - final updatedTags = _calculateAvailableTags(allTags, tags); + + if (event.index < 0 || event.index >= tags.length) return; + + tags[event.index] = tags[event.index].copyWith( + tag: event.tag.tag, + uuid: event.tag.uuid, + product: event.tag.product, + internalId: event.tag.internalId, + location: event.tag.location, + ); + + final updatedTags = _calculateAvailableTags(projectTags ?? [], tags); emit(AssignTagModelLoaded( tags: tags, @@ -78,15 +81,13 @@ class AssignTagModelBloc on((event, emit) { final currentState = state; - if (currentState is AssignTagModelLoaded && - currentState.tags.isNotEmpty) { + if (currentState is AssignTagModelLoaded && currentState.tags.isNotEmpty) { final tags = List.from(currentState.tags); // Use copyWith for immutability - tags[event.index] = - tags[event.index].copyWith(location: event.location); + tags[event.index] = tags[event.index].copyWith(location: event.location); - final updatedTags = _calculateAvailableTags(allTags, tags); + final updatedTags = _calculateAvailableTags(projectTags, tags); emit(AssignTagModelLoaded( tags: tags, @@ -100,13 +101,12 @@ class AssignTagModelBloc on((event, emit) { final currentState = state; - if (currentState is AssignTagModelLoaded && - currentState.tags.isNotEmpty) { + if (currentState is AssignTagModelLoaded && currentState.tags.isNotEmpty) { final tags = List.from(currentState.tags); emit(AssignTagModelLoaded( tags: tags, - updatedTags: _calculateAvailableTags(allTags, tags), + updatedTags: _calculateAvailableTags(projectTags, tags), isSaveEnabled: _validateTags(tags), errorMessage: _getValidationError(tags), )); @@ -116,12 +116,10 @@ class AssignTagModelBloc on((event, emit) { final currentState = state; - if (currentState is AssignTagModelLoaded && - currentState.tags.isNotEmpty) { - final tags = List.from(currentState.tags) - ..remove(event.tagToDelete); + if (currentState is AssignTagModelLoaded && currentState.tags.isNotEmpty) { + final tags = List.from(currentState.tags)..remove(event.tagToDelete); - final updatedTags = _calculateAvailableTags(allTags, tags); + final updatedTags = _calculateAvailableTags(projectTags, tags); emit(AssignTagModelLoaded( tags: tags, @@ -143,10 +141,8 @@ class AssignTagModelBloc String? _getValidationError(List tags) { // Check for duplicate tags - final nonEmptyTags = tags - .map((tag) => tag.tag?.trim() ?? '') - .where((tag) => tag.isNotEmpty) - .toList(); + final nonEmptyTags = + tags.map((tag) => tag.tag?.trim() ?? '').where((tag) => tag.isNotEmpty).toList(); final duplicateTags = nonEmptyTags .fold>({}, (map, tag) { @@ -165,15 +161,16 @@ class AssignTagModelBloc return null; } - List _calculateAvailableTags( - List allTags, List tags) { - final selectedTags = tags + List _calculateAvailableTags(List allTags, List selectedTags) { + final selectedTagSet = selectedTags .where((tag) => (tag.tag?.trim().isNotEmpty ?? false)) .map((tag) => tag.tag!.trim()) .toSet(); - final availableTags = - allTags.where((tag) => !selectedTags.contains(tag.trim())).toList(); + final availableTags = allTags + .where((tag) => tag.tag != null && !selectedTagSet.contains(tag.tag!.trim())) + .toList(); + return availableTags; } } diff --git a/lib/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_event.dart b/lib/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_event.dart index 23c70ab0..cb878bde 100644 --- a/lib/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_event.dart +++ b/lib/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_event.dart @@ -24,7 +24,7 @@ class InitializeTagModels extends AssignTagModelEvent { class UpdateTag extends AssignTagModelEvent { final int index; - final String tag; + final Tag tag; const UpdateTag({required this.index, required this.tag}); diff --git a/lib/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_state.dart b/lib/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_state.dart index 55604d3f..08168e6d 100644 --- a/lib/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_state.dart +++ b/lib/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_state.dart @@ -17,7 +17,7 @@ class AssignTagModelLoaded extends AssignTagModelState { final bool isSaveEnabled; final String? errorMessage; - final List updatedTags; + final List updatedTags; const AssignTagModelLoaded({ required this.tags, diff --git a/lib/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.dart b/lib/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.dart index 81f851ed..df978a6e 100644 --- a/lib/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.dart +++ b/lib/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/common/dialog_dropdown.dart'; -import 'package:syncrow_web/common/dialog_textfield_dropdown.dart'; +import 'package:syncrow_web/common/tag_dialog_textfield_dropdown.dart'; import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; @@ -53,12 +53,11 @@ class AssignTagModelsDialog extends StatelessWidget { @override Widget build(BuildContext context) { - print(projectTags); final List locations = (subspaces ?? []).map((subspace) => subspace.subspaceName).toList()..add('Main Space'); return BlocProvider( - create: (_) => AssignTagModelBloc(allTags ?? [], projectTags) + create: (_) => AssignTagModelBloc( projectTags) ..add(InitializeTagModels( initialTags: initialTags, addedProducts: addedProducts, @@ -174,12 +173,14 @@ class AssignTagModelsDialog extends StatelessWidget { child: SizedBox( width: double.infinity, // Ensure full width for dropdown - child: DialogTextfieldDropdown( - key: ValueKey('dropdown_${Uuid().v4()}_$index'), + child: TagDialogTextfieldDropdown( + key: ValueKey( + 'dropdown_${const Uuid().v4()}_$index'), + product: tag.product?.uuid ?? 'Unknown', items: state.updatedTags, - initialValue: tag.tag, + initialValue: tag, onSelected: (value) { - controller.text = value; + controller.text = value.tag ?? ''; context.read().add(UpdateTag( index: index, tag: value, diff --git a/lib/pages/spaces_management/space_model/models/create_space_template_body_model.dart b/lib/pages/spaces_management/space_model/models/create_space_template_body_model.dart index ad0770d5..161bbf3d 100644 --- a/lib/pages/spaces_management/space_model/models/create_space_template_body_model.dart +++ b/lib/pages/spaces_management/space_model/models/create_space_template_body_model.dart @@ -6,7 +6,7 @@ class TagBodyModel { Map toJson() { return { 'uuid': uuid, - 'tag': tag, + 'name': tag, 'productUuid': productUuid, }; }