diff --git a/lib/pages/space_management_v2/main_module/views/space_management_page.dart b/lib/pages/space_management_v2/main_module/views/space_management_page.dart index 55e47de1..47a67c36 100644 --- a/lib/pages/space_management_v2/main_module/views/space_management_page.dart +++ b/lib/pages/space_management_v2/main_module/views/space_management_page.dart @@ -9,6 +9,8 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/presen import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_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/presentation/bloc/products_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/data/services/remote_reorder_spaces_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_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_space_details_spaces_decorator_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart'; @@ -25,15 +27,16 @@ class SpaceManagementPage extends StatefulWidget { class _SpaceManagementPageState extends State { late final CommunitiesBloc communitiesBloc; + late final HTTPService _httpService; @override void initState() { + _httpService = HTTPService(); communitiesBloc = CommunitiesBloc( communitiesService: DebouncedCommunitiesService( - RemoteCommunitiesService(HTTPService()), + RemoteCommunitiesService(_httpService), ), )..add(const LoadCommunities(LoadCommunitiesParam())); - super.initState(); } @@ -50,13 +53,18 @@ class _SpaceManagementPageState extends State { BlocProvider( create: (context) => SpaceDetailsBloc( UniqueSpaceDetailsSpacesDecoratorService( - RemoteSpaceDetailsService(httpService: HTTPService()), + RemoteSpaceDetailsService(httpService: _httpService), ), ), ), BlocProvider( create: (context) => ProductsBloc( - RemoteProductsService(HTTPService()), + RemoteProductsService(_httpService), + ), + ), + BlocProvider( + create: (context) => ReorderSpacesBloc( + RemoteReorderSpacesService(_httpService), ), ), ], diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart index 692ffc0a..6614aa88 100644 --- a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart @@ -11,6 +11,8 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain 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/bloc/communities_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -164,6 +166,16 @@ class _CommunityStructureCanvasState extends State context.read().add( CommunitiesUpdateCommunity(newCommunity), ); + + context.read().add( + ReorderSpacesEvent( + ReorderSpacesParam( + communityUuid: widget.community.uuid, + parentSpaceUuid: data.parent?.uuid ?? '', + spaces: children, + ), + ), + ); } void _onSpaceTapped(SpaceModel? space) { @@ -245,6 +257,13 @@ class _CommunityStructureCanvasState extends State final levelXOffset = {}; _calculateLayout(community.spaces, 0, levelXOffset); + const horizontalCanvasPadding = 100.0; + final originalPositions = Map.of(_positions); + _positions.clear(); + for (final entry in originalPositions.entries) { + _positions[entry.key] = entry.value.translate(horizontalCanvasPadding, 0); + } + final selectedSpace = widget.selectedSpace; final highlightedUuids = {}; if (selectedSpace != null) { @@ -262,7 +281,7 @@ class _CommunityStructureCanvasState extends State community: widget.community, ); - final createButtonX = levelXOffset[0] ?? 0.0; + final createButtonX = (levelXOffset[0] ?? 0.0) + horizontalCanvasPadding; const createButtonY = 0.0; widgets.add( @@ -294,10 +313,12 @@ class _CommunityStructureCanvasState extends State CommunityModel? community, SpaceModel? parent, }) { + const targetWidth = 40.0; + final padding = (_horizontalSpacing - targetWidth) / 2; if (spaces.isNotEmpty) { final firstChildPos = _positions[spaces.first.uuid]!; final targetPos = Offset( - firstChildPos.dx - (_horizontalSpacing / 4), + firstChildPos.dx - padding - targetWidth, firstChildPos.dy, ); widgets.add(_buildDropTarget(parent, community, 0, targetPos)); @@ -379,7 +400,7 @@ class _CommunityStructureCanvasState extends State ); final targetPos = Offset( - position.dx + cardWidth + (_horizontalSpacing / 4) - 20, + position.dx + cardWidth + padding, position.dy, ); widgets.add(_buildDropTarget(parent, community, i + 1, targetPos)); @@ -414,24 +435,33 @@ class _CommunityStructureCanvasState extends State child: DragTarget( builder: (context, candidateData, rejectedData) { if (_draggedData == null) { - return const SizedBox(); + return const SizedBox.shrink(); } - final isTargetForDragged = (_draggedData?.parent?.uuid == parent?.uuid && - _draggedData?.community == null) || - (_draggedData?.community?.uuid == community?.uuid && - _draggedData?.parent == null); + final children = parent?.children ?? community?.spaces ?? []; + final isSameParent = (_draggedData!.parent?.uuid == parent?.uuid && + _draggedData!.community == null) || + (_draggedData!.community?.uuid == community?.uuid && + _draggedData!.parent == null); - if (!isTargetForDragged) { - return const SizedBox(); + if (!isSameParent) { + return const SizedBox.shrink(); } - return Container( + final oldIndex = + children.indexWhere((s) => s.uuid == _draggedData!.space.uuid); + if (oldIndex != -1 && (oldIndex == index || oldIndex == index - 1)) { + return const SizedBox.shrink(); + } + + return AnimatedContainer( + duration: const Duration(milliseconds: 150), width: 40, + alignment: Alignment.center, height: _cardHeight, decoration: BoxDecoration( color: context.theme.colorScheme.primary.withValues( - alpha: candidateData.isNotEmpty ? 0.7 : 0.3, + alpha: candidateData.isNotEmpty ? 0.9 : 0.3, ), borderRadius: BorderRadius.circular(8), ), @@ -454,6 +484,9 @@ class _CommunityStructureCanvasState extends State final oldIndex = children.indexWhere((s) => s.uuid == data.data.space.uuid); + if (oldIndex == -1) { + return true; + } if (oldIndex == index || oldIndex == index - 1) { return false; } @@ -481,7 +514,7 @@ class _CommunityStructureCanvasState extends State child: SizedBox( width: context.screenWidth * 5, height: context.screenHeight * 5, - child: Stack(children: treeWidgets), + child: Stack(clipBehavior: Clip.none, children: treeWidgets), ), ), ); diff --git a/lib/pages/space_management_v2/modules/reorder_spaces/data/services/remote_reorder_spaces_service.dart b/lib/pages/space_management_v2/modules/reorder_spaces/data/services/remote_reorder_spaces_service.dart new file mode 100644 index 00000000..c2494c09 --- /dev/null +++ b/lib/pages/space_management_v2/modules/reorder_spaces/data/services/remote_reorder_spaces_service.dart @@ -0,0 +1,58 @@ +import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/services/reorder_spaces_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 RemoteReorderSpacesService implements ReorderSpacesService { + RemoteReorderSpacesService(this._httpClient); + + final HTTPService _httpClient; + + @override + Future reorderSpaces(ReorderSpacesParam param) async { + try { + await _httpClient.post( + path: await _makeUrl(param), + body: param.toJson(), + expectedResponseModel: (json) => json, + ); + } on DioException catch (e) { + final message = e.response?.data as Map?; + throw APIException(_getErrorMessageFromBody(message)); + } catch (e) { + throw APIException(e.toString()); + } + } + + String _getErrorMessageFromBody(Map? body) { + if (body == null) return 'Failed to delete space'; + final error = body['error'] as Map?; + final errorMessage = error?['message'] as String? ?? ''; + return errorMessage; + } + + Future _makeUrl(ReorderSpacesParam param) async { + final projectUuid = await ProjectManager.getProjectUUID(); + final communityUuid = param.communityUuid; + + if (projectUuid == null || projectUuid.isEmpty) { + throw APIException('Project UUID is not set'); + } + + if (communityUuid.isEmpty) { + throw APIException('Community UUID is not set'); + } + + if (param.parentSpaceUuid.isEmpty) { + throw APIException('Parent Space UUID is not set'); + } + + return ApiEndpoints.reorderSpaces + .replaceAll('{projectUuid}', projectUuid) + .replaceAll('{communityUuid}', communityUuid) + .replaceAll('{parentSpaceUuid}', param.parentSpaceUuid); + } +} diff --git a/lib/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart b/lib/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart new file mode 100644 index 00000000..05316006 --- /dev/null +++ b/lib/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart @@ -0,0 +1,21 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; + +class ReorderSpacesParam extends Equatable { + const ReorderSpacesParam({ + required this.communityUuid, + required this.parentSpaceUuid, + required this.spaces, + }); + + final String communityUuid; + final String parentSpaceUuid; + final List spaces; + + @override + List get props => [spaces, communityUuid, parentSpaceUuid]; + + Map toJson() => { + 'spacesUuids': spaces.map((space) => space.uuid).toList(), + }; +} diff --git a/lib/pages/space_management_v2/modules/reorder_spaces/domain/services/reorder_spaces_service.dart b/lib/pages/space_management_v2/modules/reorder_spaces/domain/services/reorder_spaces_service.dart new file mode 100644 index 00000000..46811fae --- /dev/null +++ b/lib/pages/space_management_v2/modules/reorder_spaces/domain/services/reorder_spaces_service.dart @@ -0,0 +1,5 @@ +import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart'; + +abstract interface class ReorderSpacesService { + Future reorderSpaces(ReorderSpacesParam param); +} diff --git a/lib/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_bloc.dart b/lib/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_bloc.dart new file mode 100644 index 00000000..ecd15898 --- /dev/null +++ b/lib/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_bloc.dart @@ -0,0 +1,35 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/services/reorder_spaces_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; + +part 'reorder_spaces_event.dart'; +part 'reorder_spaces_state.dart'; + +class ReorderSpacesBloc extends Bloc { + ReorderSpacesBloc( + this._reorderSpacesService, + ) : super(const ReorderSpacesInitial()) { + on(_onReorderSpacesEvent); + } + + final ReorderSpacesService _reorderSpacesService; + + Future _onReorderSpacesEvent( + ReorderSpacesEvent event, + Emitter emit, + ) async { + emit(const ReorderSpacesLoading()); + try { + await _reorderSpacesService.reorderSpaces(event.param); + emit(const ReorderSpacesSuccess()); + } on APIException catch (e) { + emit(ReorderSpacesFailure(e.message)); + } catch (e) { + emit(ReorderSpacesFailure(e.toString())); + } finally { + emit(const ReorderSpacesInitial()); + } + } +} diff --git a/lib/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_event.dart b/lib/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_event.dart new file mode 100644 index 00000000..8cccb4f1 --- /dev/null +++ b/lib/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_event.dart @@ -0,0 +1,10 @@ +part of 'reorder_spaces_bloc.dart'; + +final class ReorderSpacesEvent extends Equatable { + const ReorderSpacesEvent(this.param); + + final ReorderSpacesParam param; + + @override + List get props => [param]; +} diff --git a/lib/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_state.dart b/lib/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_state.dart new file mode 100644 index 00000000..d237d93c --- /dev/null +++ b/lib/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_state.dart @@ -0,0 +1,29 @@ +part of 'reorder_spaces_bloc.dart'; + +sealed class ReorderSpacesState extends Equatable { + const ReorderSpacesState(); + + @override + List get props => []; +} + +final class ReorderSpacesInitial extends ReorderSpacesState { + const ReorderSpacesInitial(); +} + +final class ReorderSpacesLoading extends ReorderSpacesState { + const ReorderSpacesLoading(); +} + +final class ReorderSpacesSuccess extends ReorderSpacesState { + const ReorderSpacesSuccess(); +} + +final class ReorderSpacesFailure extends ReorderSpacesState { + const ReorderSpacesFailure(this.errorMessage); + + final String errorMessage; + + @override + List get props => [errorMessage]; +} diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index a53582be..9b8bc656 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -41,6 +41,8 @@ abstract class ApiEndpoints { '/projects/{projectId}/communities/{communityId}/spaces/{spaceId}'; static const String getSpaceHierarchy = '/projects/{projectId}/communities/{communityId}/spaces'; + static const String reorderSpaces = + '/projects/{projectUuid}/communities/{communityUuid}/spaces/{parentSpaceUuid}/spaces/order'; // Community Module static const String createCommunity = '/projects/{projectId}/communities';