updated tag dialogue selection

This commit is contained in:
hannathkadher
2025-03-05 22:24:45 +04:00
parent 5cea8eddb3
commit d2ff909bf2
8 changed files with 237 additions and 53 deletions

View File

@ -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<Tag> items;
final ValueChanged<Tag> 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<TagDialogTextfieldDropdown> {
bool _isOpen = false;
OverlayEntry? _overlayEntry;
final TextEditingController _controller = TextEditingController();
final FocusNode _focusNode = FocusNode();
List<Tag> _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),
),
],
),
),
);
}
}

View File

@ -60,7 +60,7 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
_cachedSpaceModels = await fetchSpaceModels();
}
await fetchSpaceModels();
await fetchTags();
emit(SpaceModelLoaded(
communities:
@ -78,7 +78,7 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
} else {
_cachedSpaceModels = await fetchSpaceModels();
}
await fetchSpaceModels();
await fetchTags();
emit(SpaceModelLoaded(
communities:

View File

@ -25,7 +25,7 @@ class Tag extends BaseTag {
return Tag(
uuid: json['uuid'] ?? '',
internalId: internalId,
tag: json['tag'] ?? '',
tag: json['name'] ?? '',
product: json['product'] != null
? ProductModel.fromMap(json['product'])
: null,
@ -34,6 +34,7 @@ class Tag extends BaseTag {
@override
Tag copyWith({
String? uuid,
String? tag,
ProductModel? product,
String? location,
@ -60,7 +61,7 @@ class Tag extends BaseTag {
extension TagModelExtensions on Tag {
TagBodyModel toTagBodyModel() {
return TagBodyModel()
..uuid = uuid ?? ''
..uuid = uuid
..tag = tag ?? ''
..productUuid = product?.uuid;
}

View File

@ -3,20 +3,17 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_event.dart';
import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_state.dart';
class AssignTagModelBloc
extends Bloc<AssignTagModelEvent, AssignTagModelState> {
final List<String> allTags;
class AssignTagModelBloc extends Bloc<AssignTagModelEvent, AssignTagModelState> {
final List<Tag> projectTags;
AssignTagModelBloc(this.allTags, this.projectTags) : super(AssignTagModelInitial()) {
AssignTagModelBloc(this.projectTags) : super(AssignTagModelInitial()) {
on<InitializeTagModels>((event, emit) {
final initialTags = event.initialTags ?? [];
final existingTagCounts = <String, int>{};
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<UpdateTag>((event, emit) {
final currentState = state;
if (currentState is AssignTagModelLoaded &&
currentState.tags.isNotEmpty) {
if (currentState is AssignTagModelLoaded && currentState.tags.isNotEmpty) {
final tags = List<Tag>.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<UpdateLocation>((event, emit) {
final currentState = state;
if (currentState is AssignTagModelLoaded &&
currentState.tags.isNotEmpty) {
if (currentState is AssignTagModelLoaded && currentState.tags.isNotEmpty) {
final tags = List<Tag>.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<ValidateTagModels>((event, emit) {
final currentState = state;
if (currentState is AssignTagModelLoaded &&
currentState.tags.isNotEmpty) {
if (currentState is AssignTagModelLoaded && currentState.tags.isNotEmpty) {
final tags = List<Tag>.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<DeleteTagModel>((event, emit) {
final currentState = state;
if (currentState is AssignTagModelLoaded &&
currentState.tags.isNotEmpty) {
final tags = List<Tag>.from(currentState.tags)
..remove(event.tagToDelete);
if (currentState is AssignTagModelLoaded && currentState.tags.isNotEmpty) {
final tags = List<Tag>.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<Tag> 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<String, int>>({}, (map, tag) {
@ -165,15 +161,16 @@ class AssignTagModelBloc
return null;
}
List<String> _calculateAvailableTags(
List<String> allTags, List<Tag> tags) {
final selectedTags = tags
List<Tag> _calculateAvailableTags(List<Tag> allTags, List<Tag> 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;
}
}

View File

@ -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});

View File

@ -17,7 +17,7 @@ class AssignTagModelLoaded extends AssignTagModelState {
final bool isSaveEnabled;
final String? errorMessage;
final List<String> updatedTags;
final List<Tag> updatedTags;
const AssignTagModelLoaded({
required this.tags,

View File

@ -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<String> 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<AssignTagModelBloc>().add(UpdateTag(
index: index,
tag: value,

View File

@ -6,7 +6,7 @@ class TagBodyModel {
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
'tag': tag,
'name': tag,
'productUuid': productUuid,
};
}