Compare commits

...

10 Commits

Author SHA1 Message Date
d65f9ceea9 Enhance AddDeviceTypeWidget to support initial product counts and update selection logic. Modify AssignTagsDialog to pass initial products from space allocations, improving user experience and maintainability. 2025-07-15 14:53:04 +03:00
f539b0ac8d Rename UniqueSubspacesDecorator to UniqueSpaceDetailsSpacesDecoratorService 2025-07-15 14:38:44 +03:00
5a3cf93748 Improved UniqueSubspacesDecorator implementation to improve handling of duplicate subspace names. 2025-07-15 14:37:16 +03:00
e740652507 Refactor PlusButtonWidget and SpaceCardWidget to improve widget structure and interaction handling. Replace GestureDetector with IconButton for better usability and update positioning logic for the PlusButtonWidget, enhancing maintainability and readability. 2025-07-15 13:11:22 +03:00
c60078c96a Refactor CommunityStructureCanvas to improve widget structure by rearranging the InteractiveViewer and GestureDetector hierarchy. This change enhances readability and maintainability while ensuring proper interaction handling. 2025-07-15 13:04:37 +03:00
903c5dd29b Refactor SpacesRecursiveHelper to improve variable naming and enhance readability. Update mapping logic to clarify the distinction between updated and non-null spaces, ensuring better maintainability of the recursive space handling. 2025-07-15 12:55:49 +03:00
df39fca050 Refactor CommunityStructureHeaderActionButtons to simplify null handling for selectedSpace and improve widget structure. Ensure buttons are always displayed when selectedSpace is not null, enhancing readability and maintainability. 2025-07-15 12:49:37 +03:00
f832c5d884 Refactor SpaceManagementCommunityStructure to improve widget structure and visibility handling. Introduce separate methods for building the canvas and empty state, enhancing readability and maintainability. 2025-07-15 12:30:24 +03:00
fa930571dc Ensure proper handling of null selectedSpace in CommunityStructureCanvas during widget updates to prevent unnecessary processing. 2025-07-15 12:27:45 +03:00
acefe7b355 Refactor RemoteDeleteSpaceService to use a private HTTPService instance and update URL construction with ApiEndpoints for improved maintainability. Update DeleteSpaceDialog to reflect changes in service initialization. 2025-07-15 11:09:04 +03:00
12 changed files with 146 additions and 87 deletions

View File

@ -7,13 +7,15 @@ abstract final class SpacesRecursiveHelper {
SpaceDetailsModel updatedSpace,
) {
return spaces.map((space) {
if (space.uuid == updatedSpace.uuid) {
final isUpdatedSpace = space.uuid == updatedSpace.uuid;
if (isUpdatedSpace) {
return space.copyWith(
spaceName: updatedSpace.spaceName,
icon: updatedSpace.icon,
);
}
if (space.children.isNotEmpty) {
final hasChildren = space.children.isNotEmpty;
if (hasChildren) {
return space.copyWith(
children: recusrivelyUpdate(space.children, updatedSpace),
);
@ -26,7 +28,7 @@ abstract final class SpacesRecursiveHelper {
List<SpaceModel> spaces,
String spaceUuid,
) {
final s = spaces.map((space) {
final updatedSpaces = spaces.map((space) {
if (space.uuid == spaceUuid) return null;
if (space.children.isNotEmpty) {
return space.copyWith(
@ -35,7 +37,7 @@ abstract final class SpacesRecursiveHelper {
}
return space;
}).toList();
return s.whereType<SpaceModel>().toList();
final nonNullSpaces = updatedSpaces.whereType<SpaceModel>().toList();
return nonNullSpaces;
}
}

View File

@ -10,7 +10,7 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/presen
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/space_details/data/services/remote_space_details_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/unique_subspaces_decorator.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/unique_space_details_spaces_decorator_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
@ -49,7 +49,7 @@ class _SpaceManagementPageState extends State<SpaceManagementPage> {
),
BlocProvider(
create: (context) => SpaceDetailsBloc(
UniqueSubspacesDecorator(
UniqueSpaceDetailsSpacesDecoratorService(
RemoteSpaceDetailsService(httpService: HTTPService()),
),
),

View File

@ -53,6 +53,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
@override
void didUpdateWidget(covariant CommunityStructureCanvas oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.selectedSpace == null) return;
if (widget.selectedSpace?.uuid != oldWidget.selectedSpace?.uuid) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
@ -452,17 +453,17 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
@override
Widget build(BuildContext context) {
final treeWidgets = _buildTreeWidgets();
return InteractiveViewer(
transformationController: _transformationController,
boundaryMargin: EdgeInsets.symmetric(
horizontal: context.screenWidth * 0.3,
vertical: context.screenHeight * 0.3,
),
minScale: 0.5,
maxScale: 3.0,
constrained: false,
child: GestureDetector(
onTap: _resetSelectionAndZoom,
return GestureDetector(
onTap: _resetSelectionAndZoom,
child: InteractiveViewer(
transformationController: _transformationController,
boundaryMargin: EdgeInsets.symmetric(
horizontal: context.screenWidth * 0.3,
vertical: context.screenHeight * 0.3,
),
minScale: 0.5,
maxScale: 3.0,
constrained: false,
child: SizedBox(
width: context.screenWidth * 5,
height: context.screenHeight * 5,

View File

@ -19,27 +19,27 @@ class CommunityStructureHeaderActionButtons extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (selectedSpace == null) return const SizedBox.shrink();
return Wrap(
alignment: WrapAlignment.end,
spacing: 10,
children: [
if (selectedSpace != null) ...[
CommunityStructureHeaderButton(
label: 'Edit',
svgAsset: Assets.editSpace,
onPressed: () => onEdit(selectedSpace!),
),
CommunityStructureHeaderButton(
label: 'Duplicate',
svgAsset: Assets.duplicate,
onPressed: () => onDuplicate(selectedSpace!),
),
CommunityStructureHeaderButton(
label: 'Delete',
svgAsset: Assets.spaceDelete,
onPressed: () => onDelete(selectedSpace!),
),
],
CommunityStructureHeaderButton(
label: 'Edit',
svgAsset: Assets.editSpace,
onPressed: () => onEdit(selectedSpace!),
),
CommunityStructureHeaderButton(
label: 'Duplicate',
svgAsset: Assets.duplicate,
onPressed: () => onDuplicate(selectedSpace!),
),
CommunityStructureHeaderButton(
label: 'Delete',
svgAsset: Assets.spaceDelete,
onPressed: () => onDelete(selectedSpace!),
),
],
);
}

View File

@ -2,31 +2,22 @@ import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class PlusButtonWidget extends StatelessWidget {
final Offset offset;
final void Function() onButtonTap;
final void Function() onTap;
const PlusButtonWidget({
required this.onTap,
super.key,
required this.offset,
required this.onButtonTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onButtonTap,
child: Container(
width: 30,
height: 30,
decoration: const BoxDecoration(
color: ColorsManager.spaceColor,
shape: BoxShape.circle,
),
child: const Icon(
Icons.add,
color: ColorsManager.whiteColors,
size: 20,
),
return IconButton.filled(
onPressed: onTap,
style: IconButton.styleFrom(backgroundColor: ColorsManager.spaceColor),
icon: const Icon(
Icons.add,
color: ColorsManager.whiteColors,
size: 20,
),
);
}

View File

@ -29,10 +29,9 @@ class _SpaceCardWidgetState extends State<SpaceCardWidget> {
widget.buildSpaceContainer(),
if (isHovered)
Positioned(
bottom: 0,
bottom: -5,
child: PlusButtonWidget(
offset: Offset.zero,
onButtonTap: widget.onTap,
onTap: widget.onTap,
),
),
],

View File

@ -3,6 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
class SpaceManagementCommunityStructure extends StatelessWidget {
@ -13,28 +15,44 @@ class SpaceManagementCommunityStructure extends StatelessWidget {
final selectionBloc = context.watch<CommunitiesTreeSelectionBloc>().state;
final selectedCommunity = selectionBloc.selectedCommunity;
final selectedSpace = selectionBloc.selectedSpace;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const CommunityStructureHeader(),
Visibility(
visible: selectedCommunity!.spaces.isNotEmpty,
replacement: _buildEmptyWidget(selectedCommunity),
child: _buildCanvas(selectedCommunity, selectedSpace),
),
],
);
}
Widget _buildCanvas(
CommunityModel selectedCommunity,
SpaceModel? selectedSpace,
) {
return Expanded(
child: CommunityStructureCanvas(
community: selectedCommunity,
selectedSpace: selectedSpace,
),
);
}
Widget _buildEmptyWidget(CommunityModel selectedCommunity) {
const spacer = Spacer(flex: 6);
return Visibility(
visible: selectedCommunity!.spaces.isNotEmpty,
replacement: Row(
return Expanded(
child: Row(
children: [
spacer,
Expanded(
child: CreateSpaceButton(communityUuid: selectedCommunity.uuid),
),
spacer
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CommunityStructureHeader(),
Expanded(
child: CommunityStructureCanvas(
community: selectedCommunity,
selectedSpace: selectedSpace,
child: CreateSpaceButton(
communityUuid: selectedCommunity.uuid,
),
),
spacer,
],
),
);

View File

@ -4,18 +4,17 @@ import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domai
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/services/delete_space_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart';
final class RemoteDeleteSpaceService implements DeleteSpaceService {
RemoteDeleteSpaceService({
required this.httpService,
});
const RemoteDeleteSpaceService(this._httpService);
final HTTPService httpService;
final HTTPService _httpService;
@override
Future<void> delete(DeleteSpaceParam param) async {
try {
await httpService.delete(
await _httpService.delete(
path: await _makeUrl(param),
expectedResponseModel: (json) {
final response = json as Map<String, dynamic>;
@ -56,6 +55,10 @@ final class RemoteDeleteSpaceService implements DeleteSpaceService {
if (param.spaceUuid.isEmpty) {
throw APIException('Space UUID is not set');
}
return '/projects/$projectUuid/communities/${param.communityUuid}/spaces/${param.spaceUuid}';
return ApiEndpoints.deleteSpace
.replaceAll('{projectId}', projectUuid)
.replaceAll('{communityId}', param.communityUuid)
.replaceAll('{spaceId}', param.spaceUuid);
}
}

View File

@ -27,7 +27,7 @@ class DeleteSpaceDialog extends StatelessWidget {
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => DeleteSpaceBloc(
RemoteDeleteSpaceService(httpService: HTTPService()),
RemoteDeleteSpaceService(HTTPService()),
),
child: Builder(
builder: (context) => Dialog(

View File

@ -2,23 +2,27 @@ import 'package:syncrow_web/pages/space_management_v2/modules/space_details/doma
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart';
class UniqueSubspacesDecorator implements SpaceDetailsService {
class UniqueSpaceDetailsSpacesDecoratorService implements SpaceDetailsService {
final SpaceDetailsService _decoratee;
const UniqueSubspacesDecorator(this._decoratee);
const UniqueSpaceDetailsSpacesDecoratorService(this._decoratee);
@override
Future<SpaceDetailsModel> getSpaceDetails(LoadSpaceDetailsParam param) async {
final response = await _decoratee.getSpaceDetails(param);
final uniqueSubspaces = <String, Subspace>{};
final duplicateNames = <String>{};
for (final subspace in response.subspaces) {
final normalizedName = subspace.name.trim().toLowerCase();
if (!uniqueSubspaces.containsKey(normalizedName)) {
if (uniqueSubspaces.containsKey(normalizedName)) {
duplicateNames.add(normalizedName);
} else {
uniqueSubspaces[normalizedName] = subspace;
}
}
duplicateNames.forEach(uniqueSubspaces.remove);
return response.copyWith(
subspaces: uniqueSubspaces.values.toList(),

View File

@ -10,7 +10,12 @@ import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class AddDeviceTypeWidget extends StatefulWidget {
const AddDeviceTypeWidget({super.key});
const AddDeviceTypeWidget({
super.key,
this.initialProducts = const [],
});
final List<Product> initialProducts;
@override
State<AddDeviceTypeWidget> createState() => _AddDeviceTypeWidgetState();
@ -18,6 +23,16 @@ class AddDeviceTypeWidget extends StatefulWidget {
class _AddDeviceTypeWidgetState extends State<AddDeviceTypeWidget> {
final Map<Product, int> _selectedProducts = {};
final Map<Product, int> _initialProductCounts = {};
@override
void initState() {
super.initState();
for (final product in widget.initialProducts) {
_initialProductCounts[product] = (_initialProductCounts[product] ?? 0) + 1;
}
_selectedProducts.addAll(_initialProductCounts);
}
void _onIncrement(Product product) {
setState(() {
@ -27,8 +42,12 @@ class _AddDeviceTypeWidgetState extends State<AddDeviceTypeWidget> {
void _onDecrement(Product product) {
setState(() {
if ((_selectedProducts[product] ?? 0) > 0) {
_selectedProducts[product] = _selectedProducts[product]! - 1;
final initialCount = _initialProductCounts[product] ?? 0;
final currentCount = _selectedProducts[product] ?? 0;
if (currentCount > initialCount) {
_selectedProducts[product] = currentCount - 1;
} else if (currentCount > 0 && initialCount == 0) {
_selectedProducts[product] = currentCount - 1;
if (_selectedProducts[product] == 0) {
_selectedProducts.remove(product);
}
@ -63,7 +82,22 @@ class _AddDeviceTypeWidgetState extends State<AddDeviceTypeWidget> {
actions: [
SpaceDetailsActionButtons(
onSave: () {
final result = _selectedProducts.entries
final resultMap = <Product, int>{};
resultMap.addAll(_selectedProducts);
for (final entry in _initialProductCounts.entries) {
final product = entry.key;
final initialCount = entry.value;
final currentCount = resultMap[product] ?? 0;
if (currentCount > initialCount) {
resultMap[product] = currentCount - initialCount;
} else {
resultMap.remove(product);
}
}
final result = resultMap.entries
.expand((entry) => List.generate(entry.value, (_) => entry.key))
.toList();
Navigator.of(context).pop(result);

View File

@ -205,7 +205,14 @@ class _AssignTagsDialogState extends State<AssignTagsDialog> {
onCancel: () async {
final newProducts = await showDialog<List<Product>>(
context: context,
builder: (context) => const AddDeviceTypeWidget(),
builder: (context) => AddDeviceTypeWidget(
initialProducts: [
..._space.productAllocations.map((e) => e.product),
..._space.subspaces
.expand((s) => s.productAllocations)
.map((e) => e.product),
],
),
);
if (newProducts == null || newProducts.isEmpty) return;