diff --git a/lib/pages/space_management_v2/main_module/models/space_reorder_data_model.dart b/lib/pages/space_management_v2/main_module/models/space_reorder_data_model.dart new file mode 100644 index 00000000..d05f22c7 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/models/space_reorder_data_model.dart @@ -0,0 +1,14 @@ +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'; + +class SpaceReorderDataModel { + const SpaceReorderDataModel({ + required this.space, + this.parent, + this.community, + }); + + final SpaceModel space; + final SpaceModel? parent; + final CommunityModel? community; +} 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 f23405bf..3cf761ad 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 @@ -1,12 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_reorder_data_model.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.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/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/space_details/presentation/helpers/space_details_dialog_helper.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; @@ -35,6 +37,7 @@ class _CommunityStructureCanvasState extends State late final TransformationController _transformationController; late final AnimationController _animationController; + SpaceReorderDataModel? _draggedData; @override void initState() { @@ -99,7 +102,7 @@ class _CommunityStructureCanvasState extends State final position = _positions[space.uuid]; if (position == null) return; - const scale = 1.5; + const scale = 1; final viewSize = context.size; if (viewSize == null) return; @@ -114,16 +117,33 @@ class _CommunityStructureCanvasState extends State _runAnimation(matrix); } + void _onReorder(SpaceReorderDataModel data, int newIndex) { + final newCommunity = widget.community.copyWith(); + final children = data.parent?.children ?? newCommunity.spaces; + final oldIndex = children.indexWhere((s) => s.uuid == data.space.uuid); + if (oldIndex != -1) { + final item = children.removeAt(oldIndex); + if (newIndex > oldIndex) { + children.insert(newIndex - 1, item); + } else { + children.insert(newIndex, item); + } + } + context.read().add( + CommunitiesUpdateCommunity(newCommunity), + ); + } + void _onSpaceTapped(SpaceModel? space) { context.read().add( SelectSpaceEvent(community: widget.community, space: space), ); } - void _resetSelectionAndZoom() { + void _resetSelectionAndZoom([CommunityModel? community]) { context.read().add( SelectSpaceEvent( - community: widget.community, + community: community ?? widget.community, space: null, ), ); @@ -196,7 +216,13 @@ class _CommunityStructureCanvasState extends State final widgets = []; final connections = []; - _generateWidgets(community.spaces, widgets, connections, highlightedUuids); + _generateWidgets( + widget.community.spaces, + widgets, + connections, + highlightedUuids, + community: widget.community, + ); final createButtonX = levelXOffset[0] ?? 0.0; const createButtonY = 0.0; @@ -225,53 +251,170 @@ class _CommunityStructureCanvasState extends State List spaces, List widgets, List connections, - Set highlightedUuids, - ) { - for (final space in spaces) { + Set highlightedUuids, { + CommunityModel? community, + SpaceModel? parent, + }) { + if (spaces.isNotEmpty) { + final firstChildPos = _positions[spaces.first.uuid]!; + final targetPos = Offset( + firstChildPos.dx - (_horizontalSpacing / 4), + firstChildPos.dy, + ); + widgets.add(_buildDropTarget(parent, community, 0, targetPos)); + } + + for (var i = 0; i < spaces.length; i++) { + final space = spaces[i]; final position = _positions[space.uuid]; - if (position == null) continue; + if (position == null) { + continue; + } final isHighlighted = highlightedUuids.contains(space.uuid); final hasNoSelectedSpace = widget.selectedSpace == null; + final spaceCard = SpaceCardWidget( + buildSpaceContainer: () { + return Opacity( + opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5, + child: Tooltip( + message: space.spaceName, + preferBelow: false, + child: SpaceCell( + onTap: () => _onSpaceTapped(space), + icon: space.icon, + name: space.spaceName, + ), + ), + ); + }, + onTap: () => SpaceDetailsDialogHelper.showCreate( + context, + communityUuid: widget.community.uuid, + ), + ); + + final reorderData = SpaceReorderDataModel( + space: space, + parent: parent, + community: community, + ); + widgets.add( Positioned( left: position.dx, top: position.dy, width: _cardWidth, height: _cardHeight, - child: SpaceCardWidget( - buildSpaceContainer: () { - return Opacity( - opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5, - child: Tooltip( - message: space.spaceName, - preferBelow: false, - child: SpaceCell( - onTap: () => _onSpaceTapped(space), - icon: space.icon, - name: space.spaceName, - ), + child: Draggable( + data: reorderData, + feedback: Material( + color: Colors.transparent, + child: Opacity( + opacity: 0.2, + child: SizedBox( + width: _cardWidth, + height: _cardHeight, + child: spaceCard, ), - ); - }, - onTap: () => SpaceDetailsDialogHelper.showCreate( - context, - communityUuid: widget.community.uuid, + ), ), + onDragStarted: () => setState(() => _draggedData = reorderData), + onDragEnd: (_) => setState(() => _draggedData = null), + onDraggableCanceled: (_, __) => setState(() => _draggedData = null), + childWhenDragging: Opacity(opacity: 0.4, child: spaceCard), + child: spaceCard, ), ), ); + final targetPos = Offset( + position.dx + _cardWidth + (_horizontalSpacing / 4) - 20, + position.dy, + ); + widgets.add(_buildDropTarget(parent, community, i + 1, targetPos)); + for (final child in space.children) { - connections.add( - SpaceConnectionModel(from: space.uuid, to: child.uuid), + connections.add(SpaceConnectionModel(from: space.uuid, to: child.uuid)); + } + + if (space.children.isNotEmpty) { + _generateWidgets( + space.children, + widgets, + connections, + highlightedUuids, + parent: space, ); } - _generateWidgets(space.children, widgets, connections, highlightedUuids); } } + Widget _buildDropTarget( + SpaceModel? parent, + CommunityModel? community, + int index, + Offset position, + ) { + return Positioned( + left: position.dx, + top: position.dy, + width: 40, + height: _cardHeight, + child: DragTarget( + builder: (context, candidateData, rejectedData) { + if (_draggedData == null) { + return const SizedBox(); + } + + final isTargetForDragged = (_draggedData?.parent?.uuid == parent?.uuid && + _draggedData?.community == null) || + (_draggedData?.community?.uuid == community?.uuid && + _draggedData?.parent == null); + + if (!isTargetForDragged) { + return const SizedBox(); + } + + return Container( + width: 40, + height: _cardHeight, + decoration: BoxDecoration( + color: context.theme.colorScheme.primary.withValues( + alpha: candidateData.isNotEmpty ? 0.7 : 0.3, + ), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.add, + color: context.theme.colorScheme.onPrimary, + ), + ); + }, + onWillAcceptWithDetails: (data) { + final children = parent?.children ?? community?.spaces ?? []; + final isSameParent = (data.data.parent?.uuid == parent?.uuid && + data.data.community == null) || + (data.data.community?.uuid == community?.uuid && + data.data.parent == null); + + if (!isSameParent) { + return false; + } + + final oldIndex = + children.indexWhere((s) => s.uuid == data.data.space.uuid); + if (oldIndex == index || oldIndex == index - 1) { + return false; + } + return true; + }, + onAcceptWithDetails: (data) => _onReorder(data.data, index), + ), + ); + } + @override Widget build(BuildContext context) { final treeWidgets = _buildTreeWidgets();