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 get props => []; +} + +final class CreateSpace extends CreateSpaceEvent { + const CreateSpace(this.param); + + final CreateSpaceParam param; + + @override + List get props => [param]; +} diff --git a/lib/pages/space_management_v2/modules/create_space/presentation/bloc/create_space_state.dart b/lib/pages/space_management_v2/modules/create_space/presentation/bloc/create_space_state.dart new file mode 100644 index 00000000..c5b035bb --- /dev/null +++ b/lib/pages/space_management_v2/modules/create_space/presentation/bloc/create_space_state.dart @@ -0,0 +1,31 @@ +part of 'create_space_bloc.dart'; + +sealed class CreateSpaceState extends Equatable { + const CreateSpaceState(); + + @override + List get props => []; +} + +final class CreateSpaceInitial extends CreateSpaceState { + const CreateSpaceInitial(); +} + +final class CreateSpaceLoading extends CreateSpaceState { + const CreateSpaceLoading(); +} + +final class CreateSpaceSuccess extends CreateSpaceState { + const CreateSpaceSuccess(this.space); + + final SpaceModel space; + + @override + List get props => [space]; +} + +final class CreateSpaceFailure extends CreateSpaceState { + const CreateSpaceFailure(this.errorMessage); + + final String errorMessage; +} diff --git a/lib/pages/space_management_v2/modules/delete_space/data/remote_delete_space_service.dart b/lib/pages/space_management_v2/modules/delete_space/data/remote_delete_space_service.dart new file mode 100644 index 00000000..5320f625 --- /dev/null +++ b/lib/pages/space_management_v2/modules/delete_space/data/remote_delete_space_service.dart @@ -0,0 +1,64 @@ +import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart'; +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 { + const RemoteDeleteSpaceService(this._httpService); + + final HTTPService _httpService; + + @override + Future delete(DeleteSpaceParam param) async { + try { + await _httpService.delete( + path: await _makeUrl(param), + expectedResponseModel: (json) { + final response = json as Map; + final hasSuccessfullyDeletedSpace = response['success'] as bool? ?? false; + + if (!hasSuccessfullyDeletedSpace) { + throw APIException('Failed to delete space'); + } + + return hasSuccessfullyDeletedSpace; + }, + ); + } 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(DeleteSpaceParam param) async { + final projectUuid = await ProjectManager.getProjectUUID(); + if (projectUuid == null) { + throw APIException('Project UUID is not set'); + } + + if (param.communityUuid.isEmpty) { + throw APIException('Community UUID is not set'); + } + + if (param.spaceUuid.isEmpty) { + throw APIException('Space UUID is not set'); + } + + return ApiEndpoints.deleteSpace + .replaceAll('{projectId}', projectUuid) + .replaceAll('{communityId}', param.communityUuid) + .replaceAll('{spaceId}', param.spaceUuid); + } +} diff --git a/lib/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart b/lib/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart new file mode 100644 index 00000000..d6781876 --- /dev/null +++ b/lib/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart @@ -0,0 +1,9 @@ +class DeleteSpaceParam { + const DeleteSpaceParam({ + required this.spaceUuid, + required this.communityUuid, + }); + + final String spaceUuid; + final String communityUuid; +} diff --git a/lib/pages/space_management_v2/modules/delete_space/domain/services/delete_space_service.dart b/lib/pages/space_management_v2/modules/delete_space/domain/services/delete_space_service.dart new file mode 100644 index 00000000..a537645c --- /dev/null +++ b/lib/pages/space_management_v2/modules/delete_space/domain/services/delete_space_service.dart @@ -0,0 +1,5 @@ +import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart'; + +abstract interface class DeleteSpaceService { + Future delete(DeleteSpaceParam param); +} diff --git a/lib/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_bloc.dart b/lib/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_bloc.dart new file mode 100644 index 00000000..6334bb33 --- /dev/null +++ b/lib/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_bloc.dart @@ -0,0 +1,31 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart'; +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'; + +part 'delete_space_event.dart'; +part 'delete_space_state.dart'; + +class DeleteSpaceBloc extends Bloc { + DeleteSpaceBloc(this._deleteSpaceService) : super(DeleteSpaceInitial()) { + on(_onDeleteSpace); + } + + final DeleteSpaceService _deleteSpaceService; + + Future _onDeleteSpace( + DeleteSpace event, + Emitter emit, + ) async { + emit(DeleteSpaceLoading()); + try { + await _deleteSpaceService.delete(event.param); + emit(const DeleteSpaceSuccess('Space deleted successfully')); + } on APIException catch (e) { + emit(DeleteSpaceFailure(e.message)); + } catch (e) { + emit(DeleteSpaceFailure(e.toString())); + } + } +} diff --git a/lib/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_event.dart b/lib/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_event.dart new file mode 100644 index 00000000..c80346e8 --- /dev/null +++ b/lib/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_event.dart @@ -0,0 +1,17 @@ +part of 'delete_space_bloc.dart'; + +sealed class DeleteSpaceEvent extends Equatable { + const DeleteSpaceEvent(); + + @override + List get props => []; +} + +final class DeleteSpace extends DeleteSpaceEvent { + const DeleteSpace(this.param); + + final DeleteSpaceParam param; + + @override + List get props => [param]; +} diff --git a/lib/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_state.dart b/lib/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_state.dart new file mode 100644 index 00000000..96b6d5b7 --- /dev/null +++ b/lib/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_state.dart @@ -0,0 +1,30 @@ +part of 'delete_space_bloc.dart'; + +sealed class DeleteSpaceState extends Equatable { + const DeleteSpaceState(); + + @override + List get props => []; +} + +final class DeleteSpaceInitial extends DeleteSpaceState {} + +final class DeleteSpaceLoading extends DeleteSpaceState {} + +final class DeleteSpaceSuccess extends DeleteSpaceState { + const DeleteSpaceSuccess(this.successMessage); + + final String successMessage; + + @override + List get props => [successMessage]; +} + +final class DeleteSpaceFailure extends DeleteSpaceState { + const DeleteSpaceFailure(this.errorMessage); + + final String errorMessage; + + @override + List get props => [errorMessage]; +} diff --git a/lib/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_dialog.dart b/lib/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_dialog.dart new file mode 100644 index 00000000..f2ddf24a --- /dev/null +++ b/lib/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_dialog.dart @@ -0,0 +1,73 @@ +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/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/data/remote_delete_space_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_dialog_form.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_loading_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_status_widget.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class DeleteSpaceDialog extends StatelessWidget { + const DeleteSpaceDialog({ + required this.space, + required this.community, + required this.onSuccess, + super.key, + }); + + final SpaceModel space; + final CommunityModel community; + final void Function() onSuccess; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => DeleteSpaceBloc( + RemoteDeleteSpaceService(HTTPService()), + ), + child: Builder( + builder: (context) => Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Container( + padding: const EdgeInsetsDirectional.all(32), + constraints: BoxConstraints( + maxWidth: context.screenWidth * 0.2, + ), + child: BlocConsumer( + listener: (context, state) { + if (state case DeleteSpaceSuccess()) onSuccess(); + }, + builder: (context, state) => switch (state) { + DeleteSpaceInitial() => DeleteSpaceDialogForm( + space: space, + communityUuid: community.uuid, + ), + DeleteSpaceLoading() => const DeleteSpaceLoadingWidget(), + DeleteSpaceSuccess() => DeleteSpaceStatusWidget( + message: state.successMessage, + icon: const Icon( + Icons.check_circle, + size: 92, + color: ColorsManager.goodGreen, + ), + ), + DeleteSpaceFailure() => DeleteSpaceStatusWidget( + message: state.errorMessage, + icon: const Icon( + Icons.error, + size: 92, + color: ColorsManager.red, + ), + ), + }, + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_dialog_form.dart b/lib/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_dialog_form.dart new file mode 100644 index 00000000..055b67b8 --- /dev/null +++ b/lib/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_dialog_form.dart @@ -0,0 +1,106 @@ +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/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_bloc.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 DeleteSpaceDialogForm extends StatelessWidget { + const DeleteSpaceDialogForm({ + required this.space, + required this.communityUuid, + super.key, + }); + + final SpaceModel space; + final String communityUuid; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset(Assets.xDelete, width: 36, height: 36), + const SizedBox(height: 16), + SelectableText( + 'Delete Space', + textAlign: TextAlign.center, + style: context.textTheme.titleLarge?.copyWith( + color: ColorsManager.blackColor, + fontWeight: FontWeight.w400, + fontSize: 24, + ), + ), + const SizedBox(height: 8), + SelectableText( + 'Are you sure you want to delete this space? This action is irreversible', + textAlign: TextAlign.center, + style: context.textTheme.bodyLarge?.copyWith( + color: ColorsManager.lightGreyColor, + fontWeight: FontWeight.w400, + fontSize: 14, + ), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: FilledButton( + style: _buildButtonStyle( + context, + color: ColorsManager.grey25, + textColor: ColorsManager.blackColor, + ), + onPressed: Navigator.of(context).pop, + child: const Text('Cancel'), + ), + ), + const SizedBox(width: 16), + Expanded( + child: FilledButton( + style: _buildButtonStyle( + context, + color: ColorsManager.semiTransparentRed, + textColor: ColorsManager.whiteColors, + ), + onPressed: () { + context.read().add( + DeleteSpace( + DeleteSpaceParam( + spaceUuid: space.uuid, + communityUuid: communityUuid, + ), + ), + ); + }, + child: const Text('Delete'), + ), + ), + ], + ), + ], + ); + } + + ButtonStyle _buildButtonStyle( + BuildContext context, { + required Color color, + required Color textColor, + }) { + return FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + backgroundColor: color, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + foregroundColor: textColor, + textStyle: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w400, + fontSize: 14, + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_loading_widget.dart b/lib/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_loading_widget.dart new file mode 100644 index 00000000..b658b3b3 --- /dev/null +++ b/lib/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_loading_widget.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +class DeleteSpaceLoadingWidget extends StatelessWidget { + const DeleteSpaceLoadingWidget({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox.square( + dimension: 32, + child: Center(child: CircularProgressIndicator()), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_status_widget.dart b/lib/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_status_widget.dart new file mode 100644 index 00000000..d597a451 --- /dev/null +++ b/lib/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_status_widget.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class DeleteSpaceStatusWidget extends StatelessWidget { + const DeleteSpaceStatusWidget({ + required this.message, + required this.icon, + super.key, + }); + + final String message; + final Widget icon; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 16, + children: [ + icon, + SelectableText( + message, + style: context.textTheme.bodyMedium?.copyWith( + color: ColorsManager.blackColor, + fontSize: 22, + ), + textAlign: TextAlign.center, + ), + FilledButton( + onPressed: Navigator.of(context).pop, + child: const Text('Close'), + ), + ], + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/data/services/unique_subspaces_decorator.dart b/lib/pages/space_management_v2/modules/space_details/data/services/unique_space_details_spaces_decorator_service.dart similarity index 65% rename from lib/pages/space_management_v2/modules/space_details/data/services/unique_subspaces_decorator.dart rename to lib/pages/space_management_v2/modules/space_details/data/services/unique_space_details_spaces_decorator_service.dart index 8309c545..da7fd4eb 100644 --- a/lib/pages/space_management_v2/modules/space_details/data/services/unique_subspaces_decorator.dart +++ b/lib/pages/space_management_v2/modules/space_details/data/services/unique_space_details_spaces_decorator_service.dart @@ -1,24 +1,29 @@ 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/subspace.dart'; 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 getSpaceDetails(LoadSpaceDetailsParam param) async { final response = await _decoratee.getSpaceDetails(param); final uniqueSubspaces = {}; + final duplicateNames = {}; 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(), diff --git a/lib/pages/space_management_v2/modules/space_details/domain/models/product_allocation.dart b/lib/pages/space_management_v2/modules/space_details/domain/models/product_allocation.dart new file mode 100644 index 00000000..250dad5c --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/domain/models/product_allocation.dart @@ -0,0 +1,47 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; +import 'package:uuid/uuid.dart'; + +class ProductAllocation extends Equatable { + final String uuid; + final Product product; + final Tag tag; + + const ProductAllocation({ + required this.uuid, + required this.product, + required this.tag, + }); + + factory ProductAllocation.fromJson(Map json) { + return ProductAllocation( + uuid: json['uuid'] as String? ?? const Uuid().v4(), + product: Product.fromJson(json['product'] as Map), + tag: Tag.fromJson(json['tag'] as Map), + ); + } + + ProductAllocation copyWith({ + String? uuid, + Product? product, + Tag? tag, + }) { + return ProductAllocation( + uuid: uuid ?? this.uuid, + product: product ?? this.product, + tag: tag ?? this.tag, + ); + } + + Map toJson() { + final isNewTag = tag.uuid.isEmpty; + return { + if (isNewTag) 'tagName': tag.name else 'tagUuid': tag.uuid, + 'productUuid': product.uuid, + }; + } + + @override + List get props => [uuid, product, tag]; +} diff --git a/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart b/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart index ec3c9f81..bd8ff714 100644 --- a/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart +++ b/lib/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart @@ -1,8 +1,7 @@ import 'package:equatable/equatable.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/product_allocation.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/subspace.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; -import 'package:uuid/uuid.dart'; class SpaceDetailsModel extends Equatable { final String uuid; @@ -26,6 +25,7 @@ class SpaceDetailsModel extends Equatable { productAllocations: [], subspaces: [], ); + factory SpaceDetailsModel.fromJson(Map json) { return SpaceDetailsModel( uuid: json['uuid'] as String, @@ -56,78 +56,21 @@ class SpaceDetailsModel extends Equatable { ); } - @override - List get props => [uuid, spaceName, icon, productAllocations, subspaces]; -} - -class ProductAllocation extends Equatable { - final String uuid; - final Product product; - final Tag tag; - - const ProductAllocation({ - required this.uuid, - required this.product, - required this.tag, - }); - - factory ProductAllocation.fromJson(Map json) { - return ProductAllocation( - uuid: json['uuid'] as String? ?? const Uuid().v4(), - product: Product.fromJson(json['product'] as Map), - tag: Tag.fromJson(json['tag'] as Map), - ); - } - - ProductAllocation copyWith({ - String? uuid, - Product? product, - Tag? tag, - }) { - return ProductAllocation( - uuid: uuid ?? this.uuid, - product: product ?? this.product, - tag: tag ?? this.tag, - ); + Map toJson() { + return { + 'spaceName': spaceName, + 'icon': icon, + 'subspaces': subspaces.map((e) => e.toJson()).toList(), + 'productAllocations': productAllocations.map((e) => e.toJson()).toList(), + }; } @override - List get props => [uuid, product, tag]; -} - -class Subspace extends Equatable { - final String uuid; - final String name; - final List productAllocations; - - const Subspace({ - required this.uuid, - required this.name, - required this.productAllocations, - }); - - factory Subspace.fromJson(Map json) { - return Subspace( - uuid: json['uuid'] as String, - name: json['subspaceName'] as String, - productAllocations: (json['productAllocations'] as List) - .map((e) => ProductAllocation.fromJson(e as Map)) - .toList(), - ); - } - - Subspace copyWith({ - String? uuid, - String? name, - List? productAllocations, - }) { - return Subspace( - uuid: uuid ?? this.uuid, - name: name ?? this.name, - productAllocations: productAllocations ?? this.productAllocations, - ); - } - - @override - List get props => [uuid, name, productAllocations]; + List get props => [ + uuid, + spaceName, + icon, + productAllocations, + subspaces, + ]; } diff --git a/lib/pages/space_management_v2/modules/space_details/domain/models/subspace.dart b/lib/pages/space_management_v2/modules/space_details/domain/models/subspace.dart new file mode 100644 index 00000000..4a962eb3 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/domain/models/subspace.dart @@ -0,0 +1,48 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/product_allocation.dart'; + +class Subspace extends Equatable { + final String uuid; + final String name; + final List productAllocations; + + const Subspace({ + required this.uuid, + required this.name, + required this.productAllocations, + }); + + factory Subspace.fromJson(Map json) { + return Subspace( + uuid: json['uuid'] as String, + name: json['subspaceName'] as String, + productAllocations: (json['productAllocations'] as List) + .map((e) => ProductAllocation.fromJson(e as Map)) + .toList(), + ); + } + + Map toJson() { + final isNewSubspace = uuid.endsWith('-NewTag'); + return { + if (!isNewSubspace) 'uuid': uuid, + 'subspaceName': name, + 'productAllocations': productAllocations.map((e) => e.toJson()).toList(), + }; + } + + Subspace copyWith({ + String? uuid, + String? name, + List? productAllocations, + }) { + return Subspace( + uuid: uuid ?? this.uuid, + name: name ?? this.name, + productAllocations: productAllocations ?? this.productAllocations, + ); + } + + @override + List get props => [uuid, name, productAllocations]; +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart index c5de7dad..45cb0e89 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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/data/services/remote_create_space_service.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/presentation/bloc/create_space_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/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart'; @@ -14,6 +17,8 @@ abstract final class SpaceDetailsDialogHelper { static void showCreate( BuildContext context, { required String communityUuid, + required void Function(SpaceModel updatedSpaceModel)? onSuccess, + String? parentUuid, }) { showDialog( context: context, @@ -24,14 +29,41 @@ abstract final class SpaceDetailsDialogHelper { RemoteSpaceDetailsService(httpService: HTTPService()), ), ), + BlocProvider( + create: (context) => CreateSpaceBloc( + RemoteCreateSpaceService(HTTPService()), + ), + ), ], child: Builder( - builder: (context) => SpaceDetailsDialog( - context: context, - title: const SelectableText('Create Space'), - spaceModel: SpaceModel.empty(), - onSave: (space) {}, - communityUuid: communityUuid, + builder: (context) => BlocListener( + listener: (context, state) => switch (state) { + CreateSpaceInitial() => null, + CreateSpaceLoading() => _onLoading(context), + CreateSpaceSuccess() => _onCreateSuccess( + context, + state.space, + onSuccess, + ), + CreateSpaceFailure() => _onError(context, state.errorMessage), + }, + child: SpaceDetailsDialog( + context: context, + title: const SelectableText('Create Space'), + spaceModel: SpaceModel.empty(), + onSave: (space) { + context.read().add( + CreateSpace( + CreateSpaceParam( + communityUuid: communityUuid, + space: space, + parentUuid: parentUuid, + ), + ), + ); + }, + communityUuid: communityUuid, + ), ), ), ), @@ -135,4 +167,14 @@ abstract final class SpaceDetailsDialogHelper { ), ); } + + static void _onCreateSuccess( + BuildContext context, + SpaceModel space, + void Function(SpaceModel updatedSpaceModel)? onSuccess, + ) { + Navigator.of(context).pop(); + Navigator.of(context).pop(); + onSuccess?.call(space); + } } diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart index 68bf68bd..719988c6 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/common/edit_chip.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/subspace.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart'; diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart index 8faac548..587c9ea7 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.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/subspace.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/space_details/presentation/widgets/sub_spaces_input.dart'; import 'package:uuid/uuid.dart'; diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart index 854b79bc..591f741c 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.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/subspace.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart index a80ddd15..6bc9f6d1 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.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/subspace.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart index bf13ffd3..e72bffde 100644 --- a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_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/domain/models/subspace.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart index 4c9990ae..2c100b15 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart @@ -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 initialProducts; @override State createState() => _AddDeviceTypeWidgetState(); @@ -18,6 +23,16 @@ class AddDeviceTypeWidget extends StatefulWidget { class _AddDeviceTypeWidgetState extends State { final Map _selectedProducts = {}; + final Map _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 { 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 { actions: [ SpaceDetailsActionButtons( onSave: () { - final result = _selectedProducts.entries + final resultMap = {}; + 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); diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart index 3f6d42ab..d14a3923 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/product_allocation.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/widgets/space_details_action_buttons.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; @@ -205,7 +206,14 @@ class _AssignTagsDialogState extends State { onCancel: () async { final newProducts = await showDialog>( 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; diff --git a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart index 6e7e2097..1711e019 100644 --- a/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart +++ b/lib/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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/product_allocation.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/subspace.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/domain/models/tag.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/bloc/tags_bloc.dart'; diff --git a/lib/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart b/lib/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart index a70d3b85..2585b1e7 100644 --- a/lib/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart +++ b/lib/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart @@ -34,7 +34,7 @@ class RemoteUpdateSpaceService implements UpdateSpaceService { } on DioException catch (e) { final message = e.response?.data as Map?; final error = message?['error'] as Map?; - final errorMessage = error?['error'] as String? ?? ''; + final errorMessage = error?['message'] as String? ?? ''; final formattedErrorMessage = [ _defaultErrorMessage, errorMessage, diff --git a/lib/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart b/lib/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart index 5dd9106d..25d54caa 100644 --- a/lib/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart +++ b/lib/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart @@ -9,34 +9,5 @@ class UpdateSpaceParam { final SpaceDetailsModel space; final String communityUuid; - Map toJson() { - return { - 'spaceName': space.spaceName, - 'icon': space.icon, - 'subspaces': space.subspaces.map((e) => e._toJson()).toList(), - 'productAllocations': - space.productAllocations.map((e) => e._toJson()).toList(), - }; - } -} - -extension _ProductAllocationToJson on ProductAllocation { - Map _toJson() { - final isNewTag = tag.uuid.isEmpty; - return { - if (isNewTag) 'tagName': tag.name else 'tagUuid': tag.uuid, - 'productUuid': product.uuid, - }; - } -} - -extension _SubspaceToJson on Subspace { - Map _toJson() { - final isNewSubspace = uuid.endsWith('-NewTag'); - return { - if (!isNewSubspace) 'uuid': uuid, - 'subspaceName': name, - 'productAllocations': productAllocations.map((e) => e._toJson()).toList(), - }; - } + Map toJson() => space.toJson(); } diff --git a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart index 15a22fda..4c11e694 100644 --- a/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart +++ b/lib/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart @@ -1,6 +1,8 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/product_allocation.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/subspace.dart'; part 'space_details_model_event.dart'; diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index f92975f3..b7ad15b5 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -517,4 +517,5 @@ class Assets { static const String emptyRangeOfAqi = 'assets/icons/empty_range_of_aqi.svg'; static const String homeIcon = 'assets/icons/home_icon.svg'; static const String groupIcon = 'assets/icons/group_icon.svg'; + static const String xDelete = 'assets/icons/x_delete.svg'; }