Merge pull request #85 from SyncrowIOT/bugfix/fix-tag-repeat

Fixed tag repeat
This commit is contained in:
hannathkadher
2025-02-05 17:47:47 +04:00
committed by GitHub
6 changed files with 117 additions and 75 deletions

View File

@ -4,7 +4,9 @@ import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_e
import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_state.dart'; import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_state.dart';
class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> { class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
AssignTagBloc() : super(AssignTagInitial()) { final List<String> allTags;
AssignTagBloc(this.allTags) : super(AssignTagInitial()) {
on<InitializeTags>((event, emit) { on<InitializeTags>((event, emit) {
final initialTags = event.initialTags ?? []; final initialTags = event.initialTags ?? [];
@ -16,25 +18,25 @@ class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
} }
} }
final allTags = <Tag>[]; final tags = <Tag>[];
for (var selectedProduct in event.addedProducts) { for (var selectedProduct in event.addedProducts) {
final existingCount = existingTagCounts[selectedProduct.productId] ?? 0; final existingCount = existingTagCounts[selectedProduct.productId] ?? 0;
if (selectedProduct.count == 0 || if (selectedProduct.count == 0 ||
selectedProduct.count <= existingCount) { selectedProduct.count <= existingCount) {
allTags.addAll(initialTags tags.addAll(initialTags
.where((tag) => tag.product?.uuid == selectedProduct.productId)); .where((tag) => tag.product?.uuid == selectedProduct.productId));
continue; continue;
} }
final missingCount = selectedProduct.count - existingCount; final missingCount = selectedProduct.count - existingCount;
allTags.addAll(initialTags tags.addAll(initialTags
.where((tag) => tag.product?.uuid == selectedProduct.productId)); .where((tag) => tag.product?.uuid == selectedProduct.productId));
if (missingCount > 0) { if (missingCount > 0) {
allTags.addAll(List.generate( tags.addAll(List.generate(
missingCount, missingCount,
(index) => Tag( (index) => Tag(
tag: '', tag: '',
@ -45,10 +47,14 @@ class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
} }
} }
final updatedTags = _calculateAvailableTags(allTags, tags);
emit(AssignTagLoaded( emit(AssignTagLoaded(
tags: allTags, tags: tags,
isSaveEnabled: _validateTags(allTags), updatedTags: updatedTags,
errorMessage: '')); isSaveEnabled: _validateTags(tags),
errorMessage: '',
));
}); });
on<UpdateTagEvent>((event, emit) { on<UpdateTagEvent>((event, emit) {
@ -56,10 +62,13 @@ class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) { if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) {
final tags = List<Tag>.from(currentState.tags); final tags = List<Tag>.from(currentState.tags);
tags[event.index].tag = event.tag; tags[event.index] = tags[event.index].copyWith(tag: event.tag);
final updatedTags = _calculateAvailableTags(allTags, tags);
emit(AssignTagLoaded( emit(AssignTagLoaded(
tags: tags, tags: tags,
updatedTags: updatedTags,
isSaveEnabled: _validateTags(tags), isSaveEnabled: _validateTags(tags),
errorMessage: _getValidationError(tags), errorMessage: _getValidationError(tags),
)); ));
@ -72,12 +81,15 @@ class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) { if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) {
final tags = List<Tag>.from(currentState.tags); final tags = List<Tag>.from(currentState.tags);
// Use copyWith for immutability // Update the location
tags[event.index] = tags[event.index] =
tags[event.index].copyWith(location: event.location); tags[event.index].copyWith(location: event.location);
final updatedTags = _calculateAvailableTags(allTags, tags);
emit(AssignTagLoaded( emit(AssignTagLoaded(
tags: tags, tags: tags,
updatedTags: updatedTags,
isSaveEnabled: _validateTags(tags), isSaveEnabled: _validateTags(tags),
errorMessage: _getValidationError(tags), errorMessage: _getValidationError(tags),
)); ));
@ -92,6 +104,7 @@ class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
emit(AssignTagLoaded( emit(AssignTagLoaded(
tags: tags, tags: tags,
updatedTags: _calculateAvailableTags(allTags, tags),
isSaveEnabled: _validateTags(tags), isSaveEnabled: _validateTags(tags),
errorMessage: _getValidationError(tags), errorMessage: _getValidationError(tags),
)); ));
@ -102,38 +115,37 @@ class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
final currentState = state; final currentState = state;
if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) { if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) {
final updatedTags = List<Tag>.from(currentState.tags) final tags = List<Tag>.from(currentState.tags)
..remove(event.tagToDelete); ..remove(event.tagToDelete);
// Recalculate available tags
final updatedTags = _calculateAvailableTags(allTags, tags);
emit(AssignTagLoaded( emit(AssignTagLoaded(
tags: updatedTags, tags: tags,
isSaveEnabled: _validateTags(updatedTags), updatedTags: updatedTags,
errorMessage: _getValidationError(updatedTags), isSaveEnabled: _validateTags(tags),
errorMessage: _getValidationError(tags),
)); ));
} else {
emit(const AssignTagLoaded(
tags: [],
isSaveEnabled: false,
errorMessage: 'Failed to delete tag'));
} }
}); });
} }
// Validate the tags for duplicates or empty values
bool _validateTags(List<Tag> tags) { bool _validateTags(List<Tag> tags) {
final uniqueTags = tags.map((tag) => tag.tag?.trim() ?? '').toSet(); final uniqueTags = tags.map((tag) => tag.tag?.trim() ?? '').toSet();
final hasEmptyTag = tags.any((tag) => (tag.tag?.trim() ?? '').isEmpty); final hasEmptyTag = tags.any((tag) => (tag.tag?.trim() ?? '').isEmpty);
final isValid = uniqueTags.length == tags.length && !hasEmptyTag; return uniqueTags.length == tags.length && !hasEmptyTag;
return isValid;
} }
// Get validation error for duplicate tags
String? _getValidationError(List<Tag> tags) { String? _getValidationError(List<Tag> tags) {
final hasEmptyTag = tags.any((tag) => (tag.tag?.trim() ?? '').isEmpty); final nonEmptyTags = tags
if (hasEmptyTag) {
return 'Tags cannot be empty.';
}
final duplicateTags = tags
.map((tag) => tag.tag?.trim() ?? '') .map((tag) => tag.tag?.trim() ?? '')
.where((tag) => tag.isNotEmpty)
.toList();
final duplicateTags = nonEmptyTags
.fold<Map<String, int>>({}, (map, tag) { .fold<Map<String, int>>({}, (map, tag) {
map[tag] = (map[tag] ?? 0) + 1; map[tag] = (map[tag] ?? 0) + 1;
return map; return map;
@ -149,4 +161,15 @@ class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
return null; return null;
} }
List<String> _calculateAvailableTags(List<String> allTags, List<Tag> tags) {
final selectedTags = tags
.where((tag) => (tag.tag?.trim().isNotEmpty ?? false))
.map((tag) => tag.tag!.trim())
.toSet();
final availableTags =
allTags.where((tag) => !selectedTags.contains(tag.trim())).toList();
return availableTags;
}
} }

View File

@ -1,6 +1,5 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
abstract class AssignTagState extends Equatable { abstract class AssignTagState extends Equatable {
const AssignTagState(); const AssignTagState();
@ -15,17 +14,21 @@ class AssignTagLoading extends AssignTagState {}
class AssignTagLoaded extends AssignTagState { class AssignTagLoaded extends AssignTagState {
final List<Tag> tags; final List<Tag> tags;
final List<String> updatedTags;
final bool isSaveEnabled; final bool isSaveEnabled;
final String? errorMessage; final String? errorMessage;
const AssignTagLoaded({ const AssignTagLoaded({
required this.tags, required this.tags,
required this.isSaveEnabled, required this.isSaveEnabled,
required this.updatedTags,
required this.errorMessage, required this.errorMessage,
}); });
@override @override
List<Object> get props => [tags, isSaveEnabled, errorMessage ?? '']; List<Object> get props =>
[tags, updatedTags, isSaveEnabled, errorMessage ?? ''];
} }
class AssignTagError extends AssignTagState { class AssignTagError extends AssignTagState {

View File

@ -14,6 +14,7 @@ import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_e
import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_state.dart'; import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_state.dart';
import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.dart'; import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:uuid/uuid.dart';
class AssignTagDialog extends StatelessWidget { class AssignTagDialog extends StatelessWidget {
final List<ProductModel>? products; final List<ProductModel>? products;
@ -47,7 +48,7 @@ class AssignTagDialog extends StatelessWidget {
..add('Main Space'); ..add('Main Space');
return BlocProvider( return BlocProvider(
create: (_) => AssignTagBloc() create: (_) => AssignTagBloc(allTags ?? [])
..add(InitializeTags( ..add(InitializeTags(
initialTags: initialTags, initialTags: initialTags,
addedProducts: addedProducts, addedProducts: addedProducts,
@ -119,8 +120,6 @@ class AssignTagDialog extends StatelessWidget {
: List.generate(state.tags.length, (index) { : List.generate(state.tags.length, (index) {
final tag = state.tags[index]; final tag = state.tags[index];
final controller = controllers[index]; final controller = controllers[index];
final availableTags = getAvailableTags(
allTags ?? [], state.tags, tag);
return DataRow( return DataRow(
cells: [ cells: [
@ -180,7 +179,9 @@ class AssignTagDialog extends StatelessWidget {
width: double width: double
.infinity, // Ensure full width for dropdown .infinity, // Ensure full width for dropdown
child: DialogTextfieldDropdown( child: DialogTextfieldDropdown(
items: availableTags, key: ValueKey(
'dropdown_${Uuid().v4()}_${index}'),
items: state.updatedTags,
initialValue: tag.tag, initialValue: tag.tag,
onSelected: (value) { onSelected: (value) {
controller.text = value; controller.text = value;
@ -306,15 +307,4 @@ class AssignTagDialog extends StatelessWidget {
), ),
); );
} }
List<String> getAvailableTags(
List<String> allTags, List<Tag> currentTags, Tag currentTag) {
List<String> availableTagsForTagModel = TagHelper.getAvailableTags<Tag>(
allTags: allTags,
currentTags: currentTags,
currentTag: currentTag,
getTag: (tag) => tag.tag ?? '',
);
return availableTagsForTagModel;
}
} }

View File

@ -5,7 +5,9 @@ import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model
class AssignTagModelBloc class AssignTagModelBloc
extends Bloc<AssignTagModelEvent, AssignTagModelState> { extends Bloc<AssignTagModelEvent, AssignTagModelState> {
AssignTagModelBloc() : super(AssignTagModelInitial()) { final List<String> allTags;
AssignTagModelBloc(this.allTags) : super(AssignTagModelInitial()) {
on<InitializeTagModels>((event, emit) { on<InitializeTagModels>((event, emit) {
final initialTags = event.initialTags ?? []; final initialTags = event.initialTags ?? [];
@ -17,25 +19,25 @@ class AssignTagModelBloc
} }
} }
final allTags = <TagModel>[]; final tags = <TagModel>[];
for (var selectedProduct in event.addedProducts) { for (var selectedProduct in event.addedProducts) {
final existingCount = existingTagCounts[selectedProduct.productId] ?? 0; final existingCount = existingTagCounts[selectedProduct.productId] ?? 0;
if (selectedProduct.count == 0 || if (selectedProduct.count == 0 ||
selectedProduct.count <= existingCount) { selectedProduct.count <= existingCount) {
allTags.addAll(initialTags tags.addAll(initialTags
.where((tag) => tag.product?.uuid == selectedProduct.productId)); .where((tag) => tag.product?.uuid == selectedProduct.productId));
continue; continue;
} }
final missingCount = selectedProduct.count - existingCount; final missingCount = selectedProduct.count - existingCount;
allTags.addAll(initialTags tags.addAll(initialTags
.where((tag) => tag.product?.uuid == selectedProduct.productId)); .where((tag) => tag.product?.uuid == selectedProduct.productId));
if (missingCount > 0) { if (missingCount > 0) {
allTags.addAll(List.generate( tags.addAll(List.generate(
missingCount, missingCount,
(index) => TagModel( (index) => TagModel(
tag: '', tag: '',
@ -46,9 +48,12 @@ class AssignTagModelBloc
} }
} }
final updatedTags = _calculateAvailableTags(allTags, tags);
emit(AssignTagModelLoaded( emit(AssignTagModelLoaded(
tags: allTags, tags: tags,
isSaveEnabled: _validateTags(allTags), updatedTags: updatedTags,
isSaveEnabled: _validateTags(tags),
errorMessage: '')); errorMessage: ''));
}); });
@ -57,9 +62,12 @@ class AssignTagModelBloc
if (currentState is AssignTagModelLoaded && if (currentState is AssignTagModelLoaded &&
currentState.tags.isNotEmpty) { currentState.tags.isNotEmpty) {
final tags = List<TagModel>.from(currentState.tags); final tags = List<TagModel>.from(currentState.tags);
tags[event.index].tag = event.tag; tags[event.index] = tags[event.index].copyWith(tag: event.tag);
final updatedTags = _calculateAvailableTags(allTags, tags);
emit(AssignTagModelLoaded( emit(AssignTagModelLoaded(
tags: tags, tags: tags,
updatedTags: updatedTags,
isSaveEnabled: _validateTags(tags), isSaveEnabled: _validateTags(tags),
errorMessage: _getValidationError(tags), errorMessage: _getValidationError(tags),
)); ));
@ -77,9 +85,13 @@ class AssignTagModelBloc
tags[event.index] = tags[event.index] =
tags[event.index].copyWith(location: event.location); tags[event.index].copyWith(location: event.location);
final updatedTags = _calculateAvailableTags(allTags, tags);
emit(AssignTagModelLoaded( emit(AssignTagModelLoaded(
tags: tags, tags: tags,
updatedTags: updatedTags,
isSaveEnabled: _validateTags(tags), isSaveEnabled: _validateTags(tags),
errorMessage: _getValidationError(tags),
)); ));
} }
}); });
@ -93,6 +105,7 @@ class AssignTagModelBloc
emit(AssignTagModelLoaded( emit(AssignTagModelLoaded(
tags: tags, tags: tags,
updatedTags: _calculateAvailableTags(allTags, tags),
isSaveEnabled: _validateTags(tags), isSaveEnabled: _validateTags(tags),
errorMessage: _getValidationError(tags), errorMessage: _getValidationError(tags),
)); ));
@ -104,24 +117,22 @@ class AssignTagModelBloc
if (currentState is AssignTagModelLoaded && if (currentState is AssignTagModelLoaded &&
currentState.tags.isNotEmpty) { currentState.tags.isNotEmpty) {
final updatedTags = List<TagModel>.from(currentState.tags) final tags = List<TagModel>.from(currentState.tags)
..remove(event.tagToDelete); ..remove(event.tagToDelete);
final updatedTags = _calculateAvailableTags(allTags, tags);
emit(AssignTagModelLoaded( emit(AssignTagModelLoaded(
tags: updatedTags, tags: tags,
isSaveEnabled: _validateTags(updatedTags), updatedTags: updatedTags,
isSaveEnabled: _validateTags(tags),
errorMessage: _getValidationError(tags),
)); ));
} else { }
emit(const AssignTagModelLoaded(
tags: [],
isSaveEnabled: false,
));
}
}); });
} }
bool _validateTags(List<TagModel> tags) { bool _validateTags(List<TagModel> tags) {
final uniqueTags = tags.map((tag) => tag.tag?.trim() ?? '').toSet(); final uniqueTags = tags.map((tag) => tag.tag?.trim() ?? '').toSet();
final hasEmptyTag = tags.any((tag) => (tag.tag?.trim() ?? '').isEmpty); final hasEmptyTag = tags.any((tag) => (tag.tag?.trim() ?? '').isEmpty);
final isValid = uniqueTags.length == tags.length && !hasEmptyTag; final isValid = uniqueTags.length == tags.length && !hasEmptyTag;
@ -129,14 +140,14 @@ class AssignTagModelBloc
} }
String? _getValidationError(List<TagModel> tags) { String? _getValidationError(List<TagModel> tags) {
final hasEmptyTag = tags.any((tag) => (tag.tag?.trim() ?? '').isEmpty);
if (hasEmptyTag) {
return 'Tags cannot be empty.';
}
// Check for duplicate tags // Check for duplicate tags
final duplicateTags = tags
final nonEmptyTags = tags
.map((tag) => tag.tag?.trim() ?? '') .map((tag) => tag.tag?.trim() ?? '')
.where((tag) => tag.isNotEmpty)
.toList();
final duplicateTags = nonEmptyTags
.fold<Map<String, int>>({}, (map, tag) { .fold<Map<String, int>>({}, (map, tag) {
map[tag] = (map[tag] ?? 0) + 1; map[tag] = (map[tag] ?? 0) + 1;
return map; return map;
@ -152,4 +163,16 @@ class AssignTagModelBloc
return null; return null;
} }
List<String> _calculateAvailableTags(
List<String> allTags, List<TagModel> tags) {
final selectedTags = tags
.where((tag) => (tag.tag?.trim().isNotEmpty ?? false))
.map((tag) => tag.tag!.trim())
.toSet();
final availableTags =
allTags.where((tag) => !selectedTags.contains(tag.trim())).toList();
return availableTags;
}
} }

View File

@ -17,14 +17,17 @@ class AssignTagModelLoaded extends AssignTagModelState {
final bool isSaveEnabled; final bool isSaveEnabled;
final String? errorMessage; final String? errorMessage;
final List<String> updatedTags;
const AssignTagModelLoaded({ const AssignTagModelLoaded({
required this.tags, required this.tags,
required this.isSaveEnabled, required this.isSaveEnabled,
required this.updatedTags,
this.errorMessage, this.errorMessage,
}); });
@override @override
List<Object?> get props => [tags, isSaveEnabled, errorMessage]; List<Object?> get props => [tags, updatedTags, isSaveEnabled, errorMessage];
} }
class AssignTagModelError extends AssignTagModelState { class AssignTagModelError extends AssignTagModelState {

View File

@ -16,6 +16,7 @@ import 'package:syncrow_web/pages/spaces_management/space_model/widgets/dialog/c
import 'package:syncrow_web/pages/spaces_management/tag_model/views/add_device_type_model_widget.dart'; import 'package:syncrow_web/pages/spaces_management/tag_model/views/add_device_type_model_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.dart'; import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.dart';
import 'package:uuid/uuid.dart';
class AssignTagModelsDialog extends StatelessWidget { class AssignTagModelsDialog extends StatelessWidget {
final List<ProductModel>? products; final List<ProductModel>? products;
@ -56,7 +57,7 @@ class AssignTagModelsDialog extends StatelessWidget {
..add('Main Space'); ..add('Main Space');
return BlocProvider( return BlocProvider(
create: (_) => AssignTagModelBloc() create: (_) => AssignTagModelBloc(allTags ?? [])
..add(InitializeTagModels( ..add(InitializeTagModels(
initialTags: initialTags, initialTags: initialTags,
addedProducts: addedProducts, addedProducts: addedProducts,
@ -134,9 +135,6 @@ class AssignTagModelsDialog extends StatelessWidget {
: List.generate(state.tags.length, (index) { : List.generate(state.tags.length, (index) {
final tag = state.tags[index]; final tag = state.tags[index];
final controller = controllers[index]; final controller = controllers[index];
final availableTags =
TagHelper.getAvailableTagModels(
allTags ?? [], state.tags, tag);
return DataRow( return DataRow(
cells: [ cells: [
@ -196,7 +194,9 @@ class AssignTagModelsDialog extends StatelessWidget {
width: double width: double
.infinity, // Ensure full width for dropdown .infinity, // Ensure full width for dropdown
child: DialogTextfieldDropdown( child: DialogTextfieldDropdown(
items: availableTags, key: ValueKey(
'dropdown_${Uuid().v4()}_${index}'),
items: state.updatedTags,
initialValue: tag.tag, initialValue: tag.tag,
onSelected: (value) { onSelected: (value) {
controller.text = value; controller.text = value;