diff --git a/assets/icons/x_delete.svg b/assets/icons/x_delete.svg
new file mode 100644
index 00000000..637f2e72
--- /dev/null
+++ b/assets/icons/x_delete.svg
@@ -0,0 +1,5 @@
+
diff --git a/lib/pages/space_management_v2/main_module/helpers/spaces_recursive_helper.dart b/lib/pages/space_management_v2/main_module/helpers/spaces_recursive_helper.dart
new file mode 100644
index 00000000..de5ce34f
--- /dev/null
+++ b/lib/pages/space_management_v2/main_module/helpers/spaces_recursive_helper.dart
@@ -0,0 +1,71 @@
+import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
+import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
+
+abstract final class SpacesRecursiveHelper {
+ const SpacesRecursiveHelper._();
+
+ static List recusrivelyUpdate(
+ List spaces,
+ SpaceDetailsModel updatedSpace,
+ ) {
+ return spaces.map((space) {
+ final isUpdatedSpace = space.uuid == updatedSpace.uuid;
+ if (isUpdatedSpace) {
+ return space.copyWith(
+ spaceName: updatedSpace.spaceName,
+ icon: updatedSpace.icon,
+ );
+ }
+ final hasChildren = space.children.isNotEmpty;
+ if (hasChildren) {
+ return space.copyWith(
+ children: recusrivelyUpdate(space.children, updatedSpace),
+ );
+ }
+ return space;
+ }).toList();
+ }
+
+ static List recusrivelyDelete(
+ List spaces,
+ String spaceUuid,
+ ) {
+ final updatedSpaces = spaces.map((space) {
+ if (space.uuid == spaceUuid) return null;
+ if (space.children.isNotEmpty) {
+ return space.copyWith(
+ children: recusrivelyDelete(space.children, spaceUuid),
+ );
+ }
+ return space;
+ }).toList();
+ final nonNullSpaces = updatedSpaces.whereType().toList();
+ return nonNullSpaces;
+ }
+
+ static List recursivelyInsert({
+ required List spaces,
+ required String parentUuid,
+ required SpaceModel newSpace,
+ }) {
+ return spaces.map((space) {
+ final isParentSpace = space.uuid == parentUuid;
+ if (isParentSpace) {
+ return space.copyWith(
+ children: [...space.children, newSpace],
+ );
+ }
+ final hasChildren = space.children.isNotEmpty;
+ if (hasChildren) {
+ return space.copyWith(
+ children: recursivelyInsert(
+ spaces: space.children,
+ parentUuid: parentUuid,
+ newSpace: newSpace,
+ ),
+ );
+ }
+ return space;
+ }).toList();
+ }
+}
diff --git a/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart b/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart
index e9fa0a15..ed797c74 100644
--- a/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart
+++ b/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart
@@ -5,13 +5,14 @@ import 'package:syncrow_web/utils/color_manager.dart';
class SpacesConnectionsArrowPainter extends CustomPainter {
final List connections;
final Map positions;
- final double cardWidth = 150.0;
+ final Map cardWidths;
final double cardHeight = 90.0;
final Set highlightedUuids;
SpacesConnectionsArrowPainter({
required this.connections,
required this.positions,
+ required this.cardWidths,
required this.highlightedUuids,
});
@@ -29,19 +30,30 @@ class SpacesConnectionsArrowPainter extends CustomPainter {
final from = positions[connection.from];
final to = positions[connection.to];
+ final fromWidth = cardWidths[connection.from] ?? 150.0;
+ final toWidth = cardWidths[connection.to] ?? 150.0;
if (from != null && to != null) {
final startPoint =
- Offset(from.dx + cardWidth / 2, from.dy + cardHeight - 10);
- final endPoint = Offset(to.dx + cardWidth / 2, to.dy);
+ Offset(from.dx + fromWidth / 2, from.dy + cardHeight - 10);
+ final endPoint = Offset(to.dx + toWidth / 2, to.dy);
final path = Path()..moveTo(startPoint.dx, startPoint.dy);
- final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 20);
- final controlPoint2 = Offset(endPoint.dx, endPoint.dy - 60);
-
- path.cubicTo(controlPoint1.dx, controlPoint1.dy, controlPoint2.dx,
- controlPoint2.dy, endPoint.dx, endPoint.dy);
+ if ((startPoint.dx - endPoint.dx).abs() < 1.0) {
+ path.lineTo(endPoint.dx, endPoint.dy);
+ } else {
+ final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 100);
+ final controlPoint2 = Offset(endPoint.dx, endPoint.dy - 100);
+ path.cubicTo(
+ controlPoint1.dx,
+ controlPoint1.dy,
+ controlPoint2.dx,
+ controlPoint2.dy,
+ endPoint.dx,
+ endPoint.dy,
+ );
+ }
canvas.drawPath(path, paint);
@@ -51,7 +63,7 @@ class SpacesConnectionsArrowPainter extends CustomPainter {
: ColorsManager.blackColor.withValues(alpha: 0.5)
..style = PaintingStyle.fill
..blendMode = BlendMode.srcIn;
- canvas.drawCircle(endPoint, 4, circlePaint);
+ canvas.drawCircle(endPoint, 6, circlePaint);
}
}
}
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 40a37891..55e47de1 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
@@ -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 {
),
BlocProvider(
create: (context) => SpaceDetailsBloc(
- UniqueSubspacesDecorator(
+ UniqueSpaceDetailsSpacesDecoratorService(
RemoteSpaceDetailsService(httpService: 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 3cf761ad..692ffc0a 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,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:syncrow_web/pages/space_management_v2/main_module/helpers/spaces_recursive_helper.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';
@@ -30,10 +31,11 @@ class CommunityStructureCanvas extends StatefulWidget {
class _CommunityStructureCanvasState extends State
with SingleTickerProviderStateMixin {
final Map _positions = {};
- final double _cardWidth = 150.0;
+ final Map _cardWidths = {};
final double _cardHeight = 90.0;
final double _horizontalSpacing = 150.0;
final double _verticalSpacing = 120.0;
+ static const double _minCardWidth = 150.0;
late final TransformationController _transformationController;
late final AnimationController _animationController;
@@ -52,6 +54,7 @@ class _CommunityStructureCanvasState extends State
@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) {
@@ -68,6 +71,34 @@ class _CommunityStructureCanvasState extends State
super.dispose();
}
+ double _calculateCardWidth(String text) {
+ final style = context.textTheme.bodyLarge?.copyWith(
+ fontWeight: FontWeight.bold,
+ );
+ final textPainter = TextPainter(
+ text: TextSpan(text: text, style: style),
+ maxLines: 1,
+ textDirection: TextDirection.ltr,
+ )..layout();
+
+ const iconWidth = 40.0;
+ const horizontalPadding = 10.0;
+ const contentPadding = 10.0;
+ final calculatedWidth =
+ iconWidth + horizontalPadding + textPainter.width + contentPadding;
+
+ return calculatedWidth.clamp(_minCardWidth, double.infinity);
+ }
+
+ void _calculateAllCardWidths(List spaces) {
+ for (final space in spaces) {
+ _cardWidths[space.uuid] = _calculateCardWidth(space.spaceName);
+ if (space.children.isNotEmpty) {
+ _calculateAllCardWidths(space.children);
+ }
+ }
+ }
+
Set _getAllDescendantUuids(SpaceModel space) {
final uuids = {};
for (final child in space.children) {
@@ -102,11 +133,12 @@ class _CommunityStructureCanvasState extends State
final position = _positions[space.uuid];
if (position == null) return;
+ final cardWidth = _cardWidths[space.uuid] ?? _minCardWidth;
const scale = 1;
final viewSize = context.size;
if (viewSize == null) return;
- final x = -position.dx * scale + (viewSize.width / 2) - (_cardWidth * scale / 2);
+ final x = -position.dx * scale + (viewSize.width / 2) - (cardWidth * scale / 2);
final y =
-position.dy * scale + (viewSize.height / 2) - (_cardHeight * scale / 2);
@@ -155,13 +187,16 @@ class _CommunityStructureCanvasState extends State
Map levelXOffset,
) {
for (final space in spaces) {
+ final cardWidth = _cardWidths[space.uuid] ?? _minCardWidth;
double childSubtreeWidth = 0;
if (space.children.isNotEmpty) {
_calculateLayout(space.children, depth + 1, levelXOffset);
final firstChildPos = _positions[space.children.first.uuid];
final lastChildPos = _positions[space.children.last.uuid];
if (firstChildPos != null && lastChildPos != null) {
- childSubtreeWidth = (lastChildPos.dx + _cardWidth) - firstChildPos.dx;
+ final lastChildWidth =
+ _cardWidths[space.children.last.uuid] ?? _minCardWidth;
+ childSubtreeWidth = (lastChildPos.dx + lastChildWidth) - firstChildPos.dx;
}
}
@@ -170,7 +205,7 @@ class _CommunityStructureCanvasState extends State
if (space.children.isNotEmpty) {
final firstChildPos = _positions[space.children.first.uuid]!;
- x = firstChildPos.dx + (childSubtreeWidth - _cardWidth) / 2;
+ x = firstChildPos.dx + (childSubtreeWidth - cardWidth) / 2;
} else {
x = currentX;
}
@@ -187,7 +222,7 @@ class _CommunityStructureCanvasState extends State
final y = depth * (_verticalSpacing + _cardHeight);
_positions[space.uuid] = Offset(x, y);
- levelXOffset[depth] = x + _cardWidth + _horizontalSpacing;
+ levelXOffset[depth] = x + cardWidth + _horizontalSpacing;
}
}
@@ -202,8 +237,11 @@ class _CommunityStructureCanvasState extends State
List _buildTreeWidgets() {
_positions.clear();
+ _cardWidths.clear();
final community = widget.community;
+ _calculateAllCardWidths(community.spaces);
+
final levelXOffset = {};
_calculateLayout(community.spaces, 0, levelXOffset);
@@ -231,7 +269,7 @@ class _CommunityStructureCanvasState extends State
Positioned(
left: createButtonX,
top: createButtonY,
- child: CreateSpaceButton(communityUuid: widget.community.uuid),
+ child: CreateSpaceButton(community: widget.community),
),
);
@@ -240,6 +278,7 @@ class _CommunityStructureCanvasState extends State
painter: SpacesConnectionsArrowPainter(
connections: connections,
positions: _positions,
+ cardWidths: _cardWidths,
highlightedUuids: highlightedUuids,
),
child: Stack(alignment: AlignmentDirectional.center, children: widgets),
@@ -271,6 +310,7 @@ class _CommunityStructureCanvasState extends State
continue;
}
+ final cardWidth = _cardWidths[space.uuid] ?? _minCardWidth;
final isHighlighted = highlightedUuids.contains(space.uuid);
final hasNoSelectedSpace = widget.selectedSpace == null;
@@ -278,20 +318,29 @@ class _CommunityStructureCanvasState extends State
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: SpaceCell(
+ onTap: () => _onSpaceTapped(space),
+ icon: space.icon,
+ name: space.spaceName,
),
);
},
onTap: () => SpaceDetailsDialogHelper.showCreate(
context,
communityUuid: widget.community.uuid,
+ parentUuid: space.uuid,
+ onSuccess: (updatedSpaceModel) {
+ final updatedSpaces = SpacesRecursiveHelper.recursivelyInsert(
+ spaces: widget.community.spaces,
+ parentUuid: space.uuid,
+ newSpace: updatedSpaceModel,
+ );
+ context.read().add(
+ CommunitiesUpdateCommunity(
+ widget.community.copyWith(spaces: updatedSpaces),
+ ),
+ );
+ },
),
);
@@ -305,7 +354,7 @@ class _CommunityStructureCanvasState extends State
Positioned(
left: position.dx,
top: position.dy,
- width: _cardWidth,
+ width: cardWidth,
height: _cardHeight,
child: Draggable(
data: reorderData,
@@ -314,7 +363,7 @@ class _CommunityStructureCanvasState extends State
child: Opacity(
opacity: 0.2,
child: SizedBox(
- width: _cardWidth,
+ width: cardWidth,
height: _cardHeight,
child: spaceCard,
),
@@ -330,7 +379,7 @@ class _CommunityStructureCanvasState extends State
);
final targetPos = Offset(
- position.dx + _cardWidth + (_horizontalSpacing / 4) - 20,
+ position.dx + cardWidth + (_horizontalSpacing / 4) - 20,
position.dy,
);
widgets.add(_buildDropTarget(parent, community, i + 1, targetPos));
@@ -418,17 +467,17 @@ class _CommunityStructureCanvasState extends State
@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,
diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_header.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_header.dart
index cb6271d1..2e1a350e 100644
--- a/lib/pages/space_management_v2/main_module/widgets/community_structure_header.dart
+++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_header.dart
@@ -2,41 +2,17 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart';
-import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.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/main_module/widgets/community_structure_header_action_buttons_composer.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/domain/models/space_details_model.dart';
-import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
+import 'package:syncrow_web/utils/extension/build_context_x.dart';
class CommunityStructureHeader extends StatelessWidget {
const CommunityStructureHeader({super.key});
- List _updateRecursive(
- List spaces,
- SpaceDetailsModel updatedSpace,
- ) {
- return spaces.map((space) {
- if (space.uuid == updatedSpace.uuid) {
- return space.copyWith(
- spaceName: updatedSpace.spaceName,
- icon: updatedSpace.icon,
- );
- }
- if (space.children.isNotEmpty) {
- return space.copyWith(
- children: _updateRecursive(space.children, updatedSpace),
- );
- }
- return space;
- }).toList();
- }
-
@override
Widget build(BuildContext context) {
- final theme = Theme.of(context);
final screenWidth = MediaQuery.of(context).size.width;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
@@ -44,9 +20,9 @@ class CommunityStructureHeader extends StatelessWidget {
color: ColorsManager.whiteColors,
boxShadow: [
BoxShadow(
- color: ColorsManager.shadowBlackColor,
- blurRadius: 8,
- offset: const Offset(0, 4),
+ color: ColorsManager.shadowBlackColor.withValues(alpha: 0.1),
+ blurRadius: 20,
+ offset: const Offset(0, 1),
),
],
),
@@ -57,7 +33,7 @@ class CommunityStructureHeader extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
- child: _buildCommunityInfo(context, theme, screenWidth),
+ child: _buildCommunityInfo(context, screenWidth),
),
const SizedBox(width: 16),
],
@@ -67,8 +43,7 @@ class CommunityStructureHeader extends StatelessWidget {
);
}
- Widget _buildCommunityInfo(
- BuildContext context, ThemeData theme, double screenWidth) {
+ Widget _buildCommunityInfo(BuildContext context, double screenWidth) {
final selectedCommunity =
context.watch().state.selectedCommunity;
final selectedSpace =
@@ -78,7 +53,7 @@ class CommunityStructureHeader extends StatelessWidget {
children: [
Text(
'Community Structure',
- style: theme.textTheme.headlineLarge?.copyWith(
+ style: context.textTheme.headlineLarge?.copyWith(
color: ColorsManager.blackColor,
),
),
@@ -91,7 +66,7 @@ class CommunityStructureHeader extends StatelessWidget {
Flexible(
child: SelectableText(
selectedCommunity.name,
- style: theme.textTheme.bodyLarge?.copyWith(
+ style: context.textTheme.bodyLarge?.copyWith(
color: ColorsManager.blackColor,
),
maxLines: 1,
@@ -115,27 +90,8 @@ class CommunityStructureHeader extends StatelessWidget {
),
),
const SizedBox(width: 8),
- CommunityStructureHeaderActionButtons(
- onDelete: (space) {},
- onDuplicate: (space) {},
- onEdit: (space) => SpaceDetailsDialogHelper.showEdit(
- context,
- spaceModel: selectedSpace!,
- communityUuid: selectedCommunity.uuid,
- onSuccess: (updatedSpaceDetails) {
- final communitiesBloc = context.read();
- final updatedSpaces = _updateRecursive(
- selectedCommunity.spaces,
- updatedSpaceDetails,
- );
-
- final community = selectedCommunity.copyWith(
- spaces: updatedSpaces,
- );
-
- communitiesBloc.add(CommunitiesUpdateCommunity(community));
- },
- ),
+ CommunityStructureHeaderActionButtonsComposer(
+ selectedCommunity: selectedCommunity,
selectedSpace: selectedSpace,
),
],
diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart
index a965c866..edeb4d8e 100644
--- a/lib/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart
+++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart
@@ -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!),
+ ),
],
);
}
diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons_composer.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons_composer.dart
new file mode 100644
index 00000000..d7403588
--- /dev/null
+++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons_composer.dart
@@ -0,0 +1,69 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:syncrow_web/pages/space_management_v2/main_module/helpers/spaces_recursive_helper.dart';
+import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.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/delete_space/presentation/widgets/delete_space_dialog.dart';
+import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
+
+class CommunityStructureHeaderActionButtonsComposer extends StatelessWidget {
+ const CommunityStructureHeaderActionButtonsComposer({
+ required this.selectedCommunity,
+ required this.selectedSpace,
+ super.key,
+ });
+ final CommunityModel selectedCommunity;
+ final SpaceModel? selectedSpace;
+
+ @override
+ Widget build(BuildContext context) {
+ return CommunityStructureHeaderActionButtons(
+ onDelete: (space) => showDialog(
+ context: context,
+ barrierDismissible: false,
+ builder: (_) => DeleteSpaceDialog(
+ space: space,
+ community: selectedCommunity,
+ onSuccess: () {
+ final updatedSpaces = SpacesRecursiveHelper.recusrivelyDelete(
+ selectedCommunity.spaces,
+ space.uuid,
+ );
+ final community = selectedCommunity.copyWith(
+ spaces: updatedSpaces,
+ );
+ context.read().add(
+ CommunitiesUpdateCommunity(community),
+ );
+ context.read().add(
+ SelectCommunityEvent(community: selectedCommunity),
+ );
+ },
+ ),
+ ),
+ onDuplicate: (space) {},
+ onEdit: (space) => SpaceDetailsDialogHelper.showEdit(
+ context,
+ spaceModel: selectedSpace!,
+ communityUuid: selectedCommunity.uuid,
+ onSuccess: (updatedSpaceDetails) {
+ final communitiesBloc = context.read();
+ final updatedSpaces = SpacesRecursiveHelper.recusrivelyUpdate(
+ selectedCommunity.spaces,
+ updatedSpaceDetails,
+ );
+
+ final community = selectedCommunity.copyWith(
+ spaces: updatedSpaces,
+ );
+
+ communitiesBloc.add(CommunitiesUpdateCommunity(community));
+ },
+ ),
+ selectedSpace: selectedSpace,
+ );
+ }
+}
diff --git a/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart b/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart
index e6dfbb15..4032c2ab 100644
--- a/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart
+++ b/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart
@@ -1,14 +1,18 @@
import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.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/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/color_manager.dart';
class CreateSpaceButton extends StatefulWidget {
const CreateSpaceButton({
- required this.communityUuid,
+ required this.community,
super.key,
});
- final String communityUuid;
+ final CommunityModel community;
@override
State createState() => _CreateSpaceButtonState();
@@ -25,7 +29,21 @@ class _CreateSpaceButtonState extends State {
child: InkWell(
onTap: () => SpaceDetailsDialogHelper.showCreate(
context,
- communityUuid: widget.communityUuid,
+ communityUuid: widget.community.uuid,
+ onSuccess: (updatedSpaceModel) {
+ final newCommunity = widget.community.copyWith(
+ spaces: [...widget.community.spaces, updatedSpaceModel],
+ );
+ context.read().add(
+ CommunitiesUpdateCommunity(newCommunity),
+ );
+ context.read().add(
+ SelectSpaceEvent(
+ space: updatedSpaceModel,
+ community: newCommunity,
+ ),
+ );
+ },
),
child: MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
diff --git a/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart b/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart
index 68169861..236b73c9 100644
--- a/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart
+++ b/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart
@@ -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,
),
);
}
diff --git a/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart b/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart
index 54902280..da79b817 100644
--- a/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart
+++ b/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart
@@ -29,10 +29,9 @@ class _SpaceCardWidgetState extends State {
widget.buildSpaceContainer(),
if (isHovered)
Positioned(
- bottom: 0,
+ bottom: -5,
child: PlusButtonWidget(
- offset: Offset.zero,
- onButtonTap: widget.onTap,
+ onTap: widget.onTap,
),
),
],
diff --git a/lib/pages/space_management_v2/main_module/widgets/space_cell.dart b/lib/pages/space_management_v2/main_module/widgets/space_cell.dart
index 80b18526..3eb6d5df 100644
--- a/lib/pages/space_management_v2/main_module/widgets/space_cell.dart
+++ b/lib/pages/space_management_v2/main_module/widgets/space_cell.dart
@@ -20,21 +20,19 @@ class SpaceCell extends StatelessWidget {
return InkWell(
onTap: onTap,
child: Container(
- width: 150,
+ padding: const EdgeInsetsDirectional.only(end: 10),
height: 70,
decoration: _containerDecoration(),
child: Row(
+ spacing: 10,
+ mainAxisSize: MainAxisSize.min,
children: [
_buildIconContainer(),
- const SizedBox(width: 10),
- Expanded(
- child: Text(
- name,
- style: context.textTheme.bodyLarge?.copyWith(
- fontWeight: FontWeight.bold,
- color: ColorsManager.blackColor,
- ),
- overflow: TextOverflow.ellipsis,
+ Text(
+ name,
+ style: context.textTheme.bodyLarge?.copyWith(
+ fontWeight: FontWeight.bold,
+ color: ColorsManager.blackColor,
),
),
],
diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart
index 050eac87..11478fbe 100644
--- a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart
+++ b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart
@@ -3,6 +3,9 @@ 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/bloc/communities_bloc.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 {
@@ -10,31 +13,59 @@ class SpaceManagementCommunityStructure extends StatelessWidget {
@override
Widget build(BuildContext context) {
- final selectionBloc = context.watch().state;
- final selectedCommunity = selectionBloc.selectedCommunity;
- final selectedSpace = selectionBloc.selectedSpace;
+ return BlocBuilder(
+ builder: (context, state) {
+ final selectedCommunity = state.selectedCommunity;
+ final selectedSpace = state.selectedSpace;
+
+ if (selectedCommunity == null) {
+ return const SizedBox.shrink();
+ }
+
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const CommunityStructureHeader(),
+ BlocBuilder(
+ builder: (context, state) {
+ final community = state.communities.firstWhere(
+ (element) => element.uuid == selectedCommunity.uuid,
+ orElse: () => selectedCommunity,
+ );
+ return Visibility(
+ visible: community.spaces.isNotEmpty,
+ replacement: _buildEmptyWidget(community),
+ child: _buildCanvas(community, 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,
- ),
- ),
+ Expanded(child: CreateSpaceButton(community: selectedCommunity)),
+ spacer,
],
),
);
diff --git a/lib/pages/space_management_v2/modules/create_space/data/services/remote_create_space_service.dart b/lib/pages/space_management_v2/modules/create_space/data/services/remote_create_space_service.dart
new file mode 100644
index 00000000..768f6438
--- /dev/null
+++ b/lib/pages/space_management_v2/modules/create_space/data/services/remote_create_space_service.dart
@@ -0,0 +1,63 @@
+import 'package:dio/dio.dart';
+import 'package:syncrow_web/pages/common/bloc/project_manager.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/create_space/domain/params/create_space_param.dart';
+import 'package:syncrow_web/pages/space_management_v2/modules/create_space/domain/services/create_space_service.dart';
+import 'package:syncrow_web/services/api/api_exception.dart';
+import 'package:syncrow_web/services/api/http_service.dart';
+
+final class RemoteCreateSpaceService implements CreateSpaceService {
+ const RemoteCreateSpaceService(this._httpService);
+
+ final HTTPService _httpService;
+
+ static const _defaultErrorMessage = 'Failed to create space';
+
+ @override
+ Future createSpace(CreateSpaceParam param) async {
+ try {
+ final path = await _makeUrl(param);
+ final response = await _httpService.post(
+ path: path,
+ body: param.toJson(),
+ expectedResponseModel: (data) {
+ final response = data as Map;
+ final isSuccess = response['success'] as bool;
+ if (!isSuccess) {
+ throw APIException(response['error'] as String);
+ }
+
+ return SpaceModel.fromJson(response['data'] as Map);
+ },
+ );
+
+ return response;
+ } on DioException catch (e) {
+ final message = e.response?.data as Map?;
+ final error = message?['error'] as Map?;
+ final errorMessage = error?['error'] as String? ?? '';
+ final formattedErrorMessage = [
+ _defaultErrorMessage,
+ errorMessage,
+ ].join(': ');
+ throw APIException(formattedErrorMessage);
+ } catch (e) {
+ final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
+ throw APIException(formattedErrorMessage);
+ }
+ }
+
+ Future _makeUrl(CreateSpaceParam param) async {
+ final projectUuid = await ProjectManager.getProjectUUID();
+ if (projectUuid == null || projectUuid.isEmpty) {
+ throw APIException('Project UUID is not set');
+ }
+
+ final communityUuid = param.communityUuid;
+ if (communityUuid.isEmpty) {
+ throw APIException('Community UUID is not set');
+ }
+
+ return '/projects/$projectUuid/communities/$communityUuid/spaces';
+ }
+}
diff --git a/lib/pages/space_management_v2/modules/create_space/domain/params/create_space_param.dart b/lib/pages/space_management_v2/modules/create_space/domain/params/create_space_param.dart
new file mode 100644
index 00000000..90a82a6b
--- /dev/null
+++ b/lib/pages/space_management_v2/modules/create_space/domain/params/create_space_param.dart
@@ -0,0 +1,22 @@
+import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
+
+class CreateSpaceParam {
+ final String communityUuid;
+ final SpaceDetailsModel space;
+ final String? parentUuid;
+
+ const CreateSpaceParam({
+ required this.communityUuid,
+ required this.space,
+ required this.parentUuid,
+ });
+
+ Map toJson() {
+ return {
+ 'parentUuid': parentUuid,
+ ...space.toJson(),
+ 'x': 0,
+ 'y': 0,
+ };
+ }
+}
diff --git a/lib/pages/space_management_v2/modules/create_space/domain/services/create_space_service.dart b/lib/pages/space_management_v2/modules/create_space/domain/services/create_space_service.dart
new file mode 100644
index 00000000..553b87e7
--- /dev/null
+++ b/lib/pages/space_management_v2/modules/create_space/domain/services/create_space_service.dart
@@ -0,0 +1,6 @@
+import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
+import 'package:syncrow_web/pages/space_management_v2/modules/create_space/domain/params/create_space_param.dart';
+
+abstract interface class CreateSpaceService {
+ Future createSpace(CreateSpaceParam param);
+}
diff --git a/lib/pages/space_management_v2/modules/create_space/presentation/bloc/create_space_bloc.dart b/lib/pages/space_management_v2/modules/create_space/presentation/bloc/create_space_bloc.dart
new file mode 100644
index 00000000..46a8abb8
--- /dev/null
+++ b/lib/pages/space_management_v2/modules/create_space/presentation/bloc/create_space_bloc.dart
@@ -0,0 +1,34 @@
+import 'package:bloc/bloc.dart';
+import 'package:equatable/equatable.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/create_space/domain/params/create_space_param.dart';
+import 'package:syncrow_web/pages/space_management_v2/modules/create_space/domain/services/create_space_service.dart';
+import 'package:syncrow_web/services/api/api_exception.dart';
+
+part 'create_space_event.dart';
+part 'create_space_state.dart';
+
+class CreateSpaceBloc extends Bloc {
+ CreateSpaceBloc(
+ this._createSpaceService,
+ ) : super(const CreateSpaceInitial()) {
+ on(_onCreateSpace);
+ }
+
+ final CreateSpaceService _createSpaceService;
+
+ Future _onCreateSpace(
+ CreateSpace event,
+ Emitter emit,
+ ) async {
+ emit(const CreateSpaceLoading());
+ try {
+ final result = await _createSpaceService.createSpace(event.param);
+ emit(CreateSpaceSuccess(result));
+ } on APIException catch (e) {
+ emit(CreateSpaceFailure(e.message));
+ } catch (e) {
+ emit(CreateSpaceFailure(e.toString()));
+ }
+ }
+}
diff --git a/lib/pages/space_management_v2/modules/create_space/presentation/bloc/create_space_event.dart b/lib/pages/space_management_v2/modules/create_space/presentation/bloc/create_space_event.dart
new file mode 100644
index 00000000..09ef8698
--- /dev/null
+++ b/lib/pages/space_management_v2/modules/create_space/presentation/bloc/create_space_event.dart
@@ -0,0 +1,17 @@
+part of 'create_space_bloc.dart';
+
+sealed class CreateSpaceEvent extends Equatable {
+ const CreateSpaceEvent();
+
+ @override
+ List