Refactor Tags Service and Bloc for Improved Data Handling:

- Updated RemoteTagsService to remove LoadTagsParam and fetch project UUID internally, enhancing encapsulation and reducing parameter dependency.
- Modified TagsService interface to reflect the new loading method signature.
- Adjusted TagsBloc to align with the updated service method, simplifying the loading process.
- Enhanced AssignTagsTable and AddDeviceTypeWidget to utilize the new data flow, improving maintainability and user experience.
This commit is contained in:
Faris Armoush
2025-07-07 10:50:03 +03:00
parent e917225c3d
commit e523a83912
6 changed files with 154 additions and 135 deletions

View File

@ -1,10 +1,9 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/params/load_tags_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/services/tags_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/services/tags_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart';
final class RemoteTagsService implements TagsService { final class RemoteTagsService implements TagsService {
const RemoteTagsService(this._httpService); const RemoteTagsService(this._httpService);
@ -14,17 +13,10 @@ final class RemoteTagsService implements TagsService {
static const _defaultErrorMessage = 'Failed to load tags'; static const _defaultErrorMessage = 'Failed to load tags';
@override @override
Future<List<Tag>> loadTags(LoadTagsParam param) async { Future<List<Tag>> loadTags() async {
if (param.projectUuid == null) {
throw Exception('Project UUID is required');
}
try { try {
final response = await _httpService.get( final response = await _httpService.get(
path: ApiEndpoints.listTags.replaceAll( path: await _makeUrl(),
'{projectUuid}',
param.projectUuid!,
),
expectedResponseModel: (json) { expectedResponseModel: (json) {
final result = json as Map<String, dynamic>; final result = json as Map<String, dynamic>;
final data = result['data'] as List<dynamic>; final data = result['data'] as List<dynamic>;
@ -46,4 +38,12 @@ final class RemoteTagsService implements TagsService {
throw APIException(formattedErrorMessage); throw APIException(formattedErrorMessage);
} }
} }
Future<String> _makeUrl() async {
final projectUuid = await ProjectManager.getProjectUUID();
if (projectUuid == null || projectUuid.isEmpty) {
throw APIException('Project UUID is required');
}
return '/projects/$projectUuid/tags';
}
} }

View File

@ -1,6 +1,5 @@
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/params/load_tags_param.dart';
abstract interface class TagsService { abstract interface class TagsService {
Future<List<Tag>> loadTags(LoadTagsParam param); Future<List<Tag>> loadTags();
} }

View File

@ -1,7 +1,6 @@
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/params/load_tags_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/services/tags_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/services/tags_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/api_exception.dart';
@ -21,7 +20,7 @@ class TagsBloc extends Bloc<TagsEvent, TagsState> {
) async { ) async {
emit(TagsLoading()); emit(TagsLoading());
try { try {
final tags = await _tagsService.loadTags(event.param); final tags = await _tagsService.loadTags();
emit(TagsLoaded(tags)); emit(TagsLoaded(tags));
} on APIException catch (e) { } on APIException catch (e) {
emit(TagsFailure(e.message)); emit(TagsFailure(e.message));

View File

@ -8,10 +8,5 @@ abstract class TagsEvent extends Equatable {
} }
class LoadTags extends TagsEvent { class LoadTags extends TagsEvent {
final LoadTagsParam param; const LoadTags();
const LoadTags(this.param);
@override
List<Object?> get props => [param];
} }

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/data/services/remote_products_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/data/services/remote_products_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart';
import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
@ -32,6 +33,13 @@ class AddDeviceTypeWidget extends StatelessWidget {
), ),
}, },
), ),
actions: [
SpaceDetailsActionButtons(
onSave: () {},
onCancel: Navigator.of(context).pop,
saveButtonLabel: 'Next',
),
],
), ),
), ),
); );

View File

@ -1,8 +1,11 @@
import 'package:flutter/material.dart'; 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_dropdown.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/data/services/remote_tags_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/bloc/tags_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -48,123 +51,138 @@ class _AssignTagsTableState extends State<AssignTagsTable> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ClipRRect( return BlocProvider<TagsBloc>(
borderRadius: BorderRadius.circular(20), create: (BuildContext context) => TagsBloc(
child: DataTable( RemoteTagsService(HTTPService()),
headingRowColor: WidgetStateProperty.all(ColorsManager.dataHeaderGrey), )..add(const LoadTags()),
key: ValueKey(widget.productAllocations.length), child: BlocBuilder<TagsBloc, TagsState>(
border: TableBorder.all( builder: (context, state) {
color: ColorsManager.dataHeaderGrey, return switch (state) {
width: 1, TagsLoading() || TagsInitial() => const Center(
borderRadius: BorderRadius.circular(20), child: CircularProgressIndicator(),
), ),
columns: [ TagsFailure(:final message) => Center(
_buildDataColumn('#'), child: Text(message),
_buildDataColumn('Device'), ),
_buildDataColumn('Tag'), TagsLoaded(:final tags) => ClipRRect(
_buildDataColumn('Location'), borderRadius: BorderRadius.circular(20),
], child: DataTable(
rows: widget.productAllocations.isEmpty headingRowColor: WidgetStateProperty.all(
? [ ColorsManager.dataHeaderGrey,
DataRow( ),
cells: [ key: ValueKey(widget.productAllocations.length),
DataCell( border: TableBorder.all(
Center( color: ColorsManager.dataHeaderGrey,
child: SelectableText( width: 1,
'No Devices Available', borderRadius: BorderRadius.circular(20),
style: context.textTheme.bodyMedium?.copyWith( ),
color: ColorsManager.lightGrayColor, columns: [
), _buildDataColumn('#'),
), _buildDataColumn('Device'),
), _buildDataColumn('Tag'),
), _buildDataColumn('Location'),
DataCell.empty,
DataCell.empty,
DataCell.empty,
], ],
), rows: widget.productAllocations.isEmpty
] ? [
: List.generate(widget.productAllocations.length, (index) { DataRow(
final productAllocation = widget.productAllocations[index]; cells: [
final controller = _controllers[index]; DataCell(
Center(
child: SelectableText(
'No Devices Available',
style: context.textTheme.bodyMedium?.copyWith(
color: ColorsManager.lightGrayColor,
),
),
),
),
DataCell.empty,
DataCell.empty,
DataCell.empty,
],
),
]
: List.generate(widget.productAllocations.length, (index) {
final productAllocation = widget.productAllocations[index];
final controller = _controllers[index];
return DataRow( return DataRow(
cells: [ cells: [
DataCell(Text((index + 1).toString())), DataCell(Text((index + 1).toString())),
DataCell( DataCell(
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
productAllocation.product.name, productAllocation.product.name,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
)), )),
const SizedBox(width: 10), const SizedBox(width: 10),
Container( Container(
width: 20, width: 20,
height: 20, height: 20,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
border: Border.all( border: Border.all(
color: ColorsManager.lightGrayColor, color: ColorsManager.lightGrayColor,
width: 1, width: 1,
),
),
child: IconButton(
icon: const Icon(
Icons.close,
color: ColorsManager.lightGreyColor,
size: 16,
),
onPressed: () {
// TODO: Delete the product allocation
},
tooltip: 'Delete Tag',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
),
],
),
), ),
), DataCell(
child: IconButton( Container(
icon: const Icon( alignment: Alignment.centerLeft,
Icons.close, width: double.infinity,
color: ColorsManager.lightGreyColor, child: ProductTagField(
size: 16, key: ValueKey(
'dropdown_${const Uuid().v4()}_$index'),
productName: productAllocation.product.uuid,
initialValue: null,
onSelected: (value) {
controller.text = value.name;
},
items: tags,
),
),
), ),
onPressed: () { DataCell(
// TODO: Delete the product allocation SizedBox(
}, width: double.infinity,
tooltip: 'Delete Tag', child: DialogDropdown(
padding: EdgeInsets.zero, items: const [],
constraints: const BoxConstraints(), // items: widget.locations,
), selectedValue:
), productAllocation.tag.name.isEmpty
], ? 'Main Space'
), : productAllocation.tag.name,
), onSelected: (value) {},
DataCell( )),
Container( ),
alignment: Alignment.centerLeft, ],
width: double.infinity, );
child: ProductTagField( }),
key: ValueKey('dropdown_${const Uuid().v4()}_$index'), ),
productName: productAllocation.product.uuid, ),
initialValue: null, _ => const SizedBox.shrink(),
onSelected: (value) { };
controller.text = value.name; },
},
items: const [
Tag(
uuid: '',
name: 'Tag',
createdAt: '',
updatedAt: '',
),
],
),
),
),
DataCell(
SizedBox(
width: double.infinity,
child: DialogDropdown(
items: const [],
// items: widget.locations,
selectedValue: productAllocation.tag.name.isEmpty
? 'Main Space'
: productAllocation.tag.name,
onSelected: (value) {},
)),
),
],
);
}),
), ),
); );
} }