fixed duplicate tag issue

This commit is contained in:
hannathkadher
2025-01-21 15:21:25 +04:00
parent 0e912207e5
commit 81e9e58627
5 changed files with 283 additions and 261 deletions

View File

@ -47,14 +47,13 @@ class AssignTagModelBloc
} }
emit(AssignTagModelLoaded( emit(AssignTagModelLoaded(
tags: allTags, tags: allTags,
isSaveEnabled: _validateTags(allTags), isSaveEnabled: _validateTags(allTags),
)); errorMessage: ''));
}); });
on<UpdateTag>((event, emit) { on<UpdateTag>((event, emit) {
final currentState = state; final currentState = state;
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);
@ -122,9 +121,7 @@ class AssignTagModelBloc
} }
bool _validateTags(List<TagModel> tags) { bool _validateTags(List<TagModel> tags) {
if (tags.isEmpty) {
return false;
}
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;
@ -133,7 +130,11 @@ class AssignTagModelBloc
String? _getValidationError(List<TagModel> tags) { String? _getValidationError(List<TagModel> tags) {
final hasEmptyTag = tags.any((tag) => (tag.tag?.trim() ?? '').isEmpty); final hasEmptyTag = tags.any((tag) => (tag.tag?.trim() ?? '').isEmpty);
if (hasEmptyTag) return 'Tags cannot be empty.'; if (hasEmptyTag) {
return 'Tags cannot be empty.';
}
// Check for duplicate tags
final duplicateTags = tags final duplicateTags = tags
.map((tag) => tag.tag?.trim() ?? '') .map((tag) => tag.tag?.trim() ?? '')
.fold<Map<String, int>>({}, (map, tag) { .fold<Map<String, int>>({}, (map, tag) {

View File

@ -5,7 +5,7 @@ abstract class AssignTagModelState extends Equatable {
const AssignTagModelState(); const AssignTagModelState();
@override @override
List<Object> get props => []; List<Object?> get props => [];
} }
class AssignTagModelInitial extends AssignTagModelState {} class AssignTagModelInitial extends AssignTagModelState {}
@ -15,7 +15,7 @@ class AssignTagModelLoading extends AssignTagModelState {}
class AssignTagModelLoaded extends AssignTagModelState { class AssignTagModelLoaded extends AssignTagModelState {
final List<TagModel> tags; final List<TagModel> tags;
final bool isSaveEnabled; final bool isSaveEnabled;
final String? errorMessage; final String? errorMessage;
const AssignTagModelLoaded({ const AssignTagModelLoaded({
required this.tags, required this.tags,
@ -24,7 +24,7 @@ class AssignTagModelLoaded extends AssignTagModelState {
}); });
@override @override
List<Object> get props => [tags, isSaveEnabled]; List<Object?> get props => [tags, isSaveEnabled, errorMessage];
} }
class AssignTagModelError extends AssignTagModelState { class AssignTagModelError extends AssignTagModelState {
@ -33,5 +33,5 @@ class AssignTagModelError extends AssignTagModelState {
const AssignTagModelError(this.errorMessage); const AssignTagModelError(this.errorMessage);
@override @override
List<Object> get props => [errorMessage]; List<Object?> get props => [errorMessage];
} }

View File

@ -47,249 +47,202 @@ class AssignTagModelsDialog extends StatelessWidget {
..add('Main Space'); ..add('Main Space');
return BlocProvider( return BlocProvider(
create: (_) => AssignTagModelBloc() create: (_) => AssignTagModelBloc()
..add(InitializeTagModels( ..add(InitializeTagModels(
initialTags: initialTags, initialTags: initialTags,
addedProducts: addedProducts, addedProducts: addedProducts,
)), )),
child: BlocBuilder<AssignTagModelBloc, AssignTagModelState>( child: BlocListener<AssignTagModelBloc, AssignTagModelState>(
builder: (context, state) { listener: (context, state) {},
if (state is AssignTagModelLoaded) { child: BlocBuilder<AssignTagModelBloc, AssignTagModelState>(
final controllers = List.generate( builder: (context, state) {
state.tags.length, if (state is AssignTagModelLoaded) {
(index) => TextEditingController(text: state.tags[index].tag), final controllers = List.generate(
); state.tags.length,
(index) => TextEditingController(text: state.tags[index].tag),
);
return AlertDialog( return AlertDialog(
title: Text(title), title: Text(title),
backgroundColor: ColorsManager.whiteColors, backgroundColor: ColorsManager.whiteColors,
content: SingleChildScrollView( content: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(20),
child: DataTable(
headingRowColor: WidgetStateProperty.all(
ColorsManager.dataHeaderGrey),
border: TableBorder.all(
color: ColorsManager.dataHeaderGrey,
width: 1,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
), child: DataTable(
columns: [ headingRowColor: WidgetStateProperty.all(
DataColumn( ColorsManager.dataHeaderGrey),
label: Text('#', border: TableBorder.all(
style: color: ColorsManager.dataHeaderGrey,
Theme.of(context).textTheme.bodyMedium)), width: 1,
DataColumn( borderRadius: BorderRadius.circular(20),
label: Text('Device', ),
style: columns: [
Theme.of(context).textTheme.bodyMedium)), DataColumn(
DataColumn( label: Text('#',
numeric: false, style: Theme.of(context)
headingRowAlignment: MainAxisAlignment.start, .textTheme
label: Text('Tag', .bodyMedium)),
style: DataColumn(
Theme.of(context).textTheme.bodyMedium)), label: Text('Device',
DataColumn( style: Theme.of(context)
label: Text('Location', .textTheme
style: .bodyMedium)),
Theme.of(context).textTheme.bodyMedium)), DataColumn(
], numeric: false,
rows: state.tags.isEmpty headingRowAlignment: MainAxisAlignment.start,
? [ label: Text('Tag',
const DataRow(cells: [ style: Theme.of(context)
DataCell( .textTheme
Center( .bodyMedium)),
child: Text( DataColumn(
'No Data Available', label: Text('Location',
style: TextStyle( style: Theme.of(context)
fontSize: 14, .textTheme
color: ColorsManager.lightGrayColor, .bodyMedium)),
],
rows: state.tags.isEmpty
? [
const DataRow(cells: [
DataCell(
Center(
child: Text(
'No Data Available',
style: TextStyle(
fontSize: 14,
color:
ColorsManager.lightGrayColor,
),
),
), ),
), ),
), DataCell(SizedBox()),
), DataCell(SizedBox()),
DataCell(SizedBox()), DataCell(SizedBox()),
DataCell(SizedBox()), ])
DataCell(SizedBox()), ]
]) : List.generate(state.tags.length, (index) {
] final tag = state.tags[index];
: List.generate(state.tags.length, (index) { final controller = controllers[index];
final tag = state.tags[index]; final availableTags = getAvailableTags(
final controller = controllers[index]; allTags ?? [], state.tags, tag);
final availableTags = getAvailableTags(
allTags ?? [], state.tags, tag);
return DataRow( return DataRow(
cells: [ cells: [
DataCell(Text(index.toString())), DataCell(Text(index.toString())),
DataCell( DataCell(
Row( Row(
mainAxisAlignment: mainAxisAlignment:
MainAxisAlignment.spaceBetween, MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
tag.product?.name ?? 'Unknown', tag.product?.name ?? 'Unknown',
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
)), )),
const SizedBox(width: 10), const SizedBox(width: 10),
Container(
width: 20.0,
height: 20.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: ColorsManager
.lightGrayColor,
width: 1.0,
),
),
child: IconButton(
icon: const Icon(
Icons.close,
color: ColorsManager
.lightGreyColor,
size: 16,
),
onPressed: () {
context
.read<
AssignTagModelBloc>()
.add(DeleteTagModel(
tagToDelete: tag,
tags: state.tags));
},
tooltip: 'Delete Tag',
padding: EdgeInsets.zero,
constraints:
const BoxConstraints(),
),
),
],
),
),
DataCell(
Container( Container(
width: 20.0, alignment: Alignment
height: 20.0, .centerLeft, // Align cell content to the left
decoration: BoxDecoration( child: SizedBox(
shape: BoxShape.circle, width: double
border: Border.all( .infinity, // Ensure full width for dropdown
color: ColorsManager child: DialogTextfieldDropdown(
.lightGrayColor, items: availableTags,
width: 1.0, initialValue: tag.tag,
onSelected: (value) {
controller.text = value;
context
.read<
AssignTagModelBloc>()
.add(UpdateTag(
index: index,
tag: value,
));
},
), ),
), ),
child: IconButton(
icon: const Icon(
Icons.close,
color: ColorsManager
.lightGreyColor,
size: 16,
),
onPressed: () {
context
.read<AssignTagModelBloc>()
.add(DeleteTagModel(
tagToDelete: tag,
tags: state.tags));
},
tooltip: 'Delete Tag',
padding: EdgeInsets.zero,
constraints:
const BoxConstraints(),
),
),
],
),
),
DataCell(
Container(
alignment: Alignment
.centerLeft, // Align cell content to the left
child: SizedBox(
width: double
.infinity, // Ensure full width for dropdown
child: DialogTextfieldDropdown(
items: availableTags,
initialValue: tag.tag,
onSelected: (value) {
controller.text = value;
context
.read<AssignTagModelBloc>()
.add(UpdateTag(
index: index,
tag: value,
));
},
), ),
), ),
), DataCell(
), SizedBox(
DataCell( width: double.infinity,
SizedBox( child: DialogDropdown(
width: double.infinity, items: locations,
child: DialogDropdown( selectedValue:
items: locations, tag.location ?? 'None',
selectedValue: onSelected: (value) {
tag.location ?? 'None', context
onSelected: (value) { .read<
context AssignTagModelBloc>()
.read<AssignTagModelBloc>() .add(UpdateLocation(
.add(UpdateLocation( index: index,
index: index, location: value,
location: value, ));
)); },
}, )),
)), ),
), ],
], );
); }),
}), ),
),
),
if (state.errorMessage != null)
Text(
state.errorMessage!,
style: const TextStyle(color: ColorsManager.warningRed),
),
],
),
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
const SizedBox(width: 10),
Expanded(
child: Builder(
builder: (buttonContext) => CancelButton(
label: 'Add New Device',
onPressed: () async {
for (var tag in state.tags) {
if (tag.location == null || subspaces == null) {
continue;
}
final previousTagSubspace =
checkTagExistInSubspace(tag, subspaces ?? []);
if (tag.location == 'Main Space') {
removeTagFromSubspace(tag, previousTagSubspace);
} else if (tag.location !=
previousTagSubspace?.subspaceName) {
removeTagFromSubspace(tag, previousTagSubspace);
moveToNewSubspace(tag, subspaces ?? []);
state.tags.removeWhere(
(t) => t.internalId == tag.internalId);
} else {
updateTagInSubspace(tag, previousTagSubspace);
state.tags.removeWhere(
(t) => t.internalId == tag.internalId);
}
}
if (context.mounted) {
await showDialog<bool>(
barrierDismissible: false,
context: context,
builder: (dialogContext) =>
AddDeviceTypeModelWidget(
products: products,
subspaces: subspaces,
isCreate: false,
initialSelectedProducts: addedProducts,
allTags: allTags,
spaceName: spaceName,
spaceTagModels: state.tags,
onUpdate: (tags, subspaces) {
onUpdate?.call(state.tags, subspaces);
Navigator.of(context).pop();
},
),
);
}
},
), ),
), if (state.errorMessage != null)
Text(
state.errorMessage!,
style: const TextStyle(
color: ColorsManager.warningRed),
),
],
), ),
const SizedBox(width: 10), ),
Expanded( actions: [
child: DefaultButton( Row(
borderRadius: 10, mainAxisAlignment: MainAxisAlignment.spaceAround,
backgroundColor: state.isSaveEnabled children: [
? ColorsManager.secondaryColor const SizedBox(width: 10),
: ColorsManager.grayColor, Expanded(
foregroundColor: ColorsManager.whiteColors, child: Builder(
onPressed: state.isSaveEnabled builder: (buttonContext) => CancelButton(
? () async { label: 'Add New Device',
Navigator.of(context).pop(); onPressed: () async {
for (var tag in state.tags) { for (var tag in state.tags) {
if (tag.location == null || if (tag.location == null ||
subspaces == null) { subspaces == null) {
@ -317,26 +270,89 @@ class AssignTagModelsDialog extends StatelessWidget {
(t) => t.internalId == tag.internalId); (t) => t.internalId == tag.internalId);
} }
} }
if (context.mounted) {
await showDialog<bool>(
barrierDismissible: false,
context: context,
builder: (dialogContext) =>
AddDeviceTypeModelWidget(
products: products,
subspaces: subspaces,
isCreate: false,
initialSelectedProducts: addedProducts,
allTags: allTags,
spaceName: spaceName,
spaceTagModels: state.tags,
onUpdate: (tags, subspaces) {
onUpdate?.call(state.tags, subspaces);
Navigator.of(context).pop();
},
),
);
}
},
),
),
),
const SizedBox(width: 10),
Expanded(
child: DefaultButton(
borderRadius: 10,
backgroundColor: state.isSaveEnabled
? ColorsManager.secondaryColor
: ColorsManager.grayColor,
foregroundColor: ColorsManager.whiteColors,
onPressed: state.isSaveEnabled
? () async {
Navigator.of(context).pop();
onUpdate?.call(state.tags, subspaces); for (var tag in state.tags) {
} if (tag.location == null ||
: null, subspaces == null) {
child: const Text('Save'), continue;
), }
final previousTagSubspace =
checkTagExistInSubspace(
tag, subspaces ?? []);
if (tag.location == 'Main Space') {
removeTagFromSubspace(
tag, previousTagSubspace);
} else if (tag.location !=
previousTagSubspace?.subspaceName) {
removeTagFromSubspace(
tag, previousTagSubspace);
moveToNewSubspace(tag, subspaces ?? []);
state.tags.removeWhere((t) =>
t.internalId == tag.internalId);
} else {
updateTagInSubspace(
tag, previousTagSubspace);
state.tags.removeWhere((t) =>
t.internalId == tag.internalId);
}
}
onUpdate?.call(state.tags, subspaces);
}
: null,
child: const Text('Save'),
),
),
const SizedBox(width: 10),
],
), ),
const SizedBox(width: 10),
], ],
), );
], } else if (state is AssignTagModelLoading) {
); return const Center(child: CircularProgressIndicator());
} else if (state is AssignTagModelLoading) { } else {
return const Center(child: CircularProgressIndicator()); return const Center(child: Text('Something went wrong.'));
} else { }
return const Center(child: Text('Something went wrong.')); },
} ),
}, ));
),
);
} }
List<String> getAvailableTags( List<String> getAvailableTags(

View File

@ -3,7 +3,6 @@ import 'package:syncrow_web/pages/spaces_management/space_model/bloc/create_spac
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/create_space_model_state.dart'; import 'package:syncrow_web/pages/spaces_management/space_model/bloc/create_space_model_state.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/create_space_template_body_model.dart'; import 'package:syncrow_web/pages/spaces_management/space_model/models/create_space_template_body_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart'; import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
import 'package:syncrow_web/services/space_model_mang_api.dart'; import 'package:syncrow_web/services/space_model_mang_api.dart';
@ -53,6 +52,8 @@ class CreateSpaceModelBloc
} }
}); });
on<LoadSpaceTemplate>((event, emit) { on<LoadSpaceTemplate>((event, emit) {
emit(CreateSpaceModelLoading()); emit(CreateSpaceModelLoading());
Future.delayed(const Duration(seconds: 1), () { Future.delayed(const Duration(seconds: 1), () {

View File

@ -166,6 +166,10 @@ class CreateSpaceModelDialog extends StatelessWidget {
modelName: modelName:
spaceNameController.text.trim(), spaceNameController.text.trim(),
); );
if(updatedSpaceTemplate.uuid != null){
}
context.read<CreateSpaceModelBloc>().add( context.read<CreateSpaceModelBloc>().add(
CreateSpaceTemplate( CreateSpaceTemplate(
spaceTemplate: spaceTemplate: