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 6614aa88..ae9f846a 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,3 +1,5 @@ +import 'dart:math'; + 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'; @@ -51,13 +53,23 @@ class _CommunityStructureCanvasState extends State duration: const Duration(milliseconds: 150), ); super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _centerOnTree(); + } + }); } @override void didUpdateWidget(covariant CommunityStructureCanvas oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.selectedSpace == null) return; - if (widget.selectedSpace?.uuid != oldWidget.selectedSpace?.uuid) { + if (oldWidget.community.uuid != widget.community.uuid) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _centerOnTree(animate: true); + } + }); + } else if (widget.selectedSpace?.uuid != oldWidget.selectedSpace?.uuid) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { _animateToSpace(widget.selectedSpace); @@ -151,6 +163,60 @@ class _CommunityStructureCanvasState extends State _runAnimation(matrix); } + void _centerOnTree({bool animate = false}) { + if (_positions.isEmpty) { + if (animate) { + _runAnimation(Matrix4.identity()); + } else { + _transformationController.value = Matrix4.identity(); + } + return; + } + + var minX = double.infinity; + var maxX = double.negativeInfinity; + var minY = double.infinity; + var maxY = double.negativeInfinity; + + _positions.forEach((uuid, offset) { + final cardWidth = _cardWidths[uuid] ?? _minCardWidth; + minX = min(minX, offset.dx); + maxX = max(maxX, offset.dx + cardWidth); + minY = min(minY, offset.dy); + maxY = max(maxY, offset.dy + _cardHeight); + }); + + if (!minX.isFinite || !maxX.isFinite || !minY.isFinite || !maxY.isFinite) { + return; + } + + final treeWidth = maxX - minX; + final treeHeight = maxY - minY; + + final viewSize = context.size; + if (viewSize == null) return; + + final scaleX = viewSize.width / treeWidth; + final scaleY = viewSize.height / treeHeight; + final scale = min(scaleX, scaleY).clamp(0.5, 1.0) * 0.9; + + final treeCenterX = minX + treeWidth / 2; + final treeCenterY = minY + treeHeight / 2; + + final x = -treeCenterX * scale + viewSize.width / 2; + final y = -treeCenterY * scale + viewSize.height / 2; + + final matrix = Matrix4.identity() + ..translate(x, y) + ..scale(scale); + + if (animate) { + _runAnimation(matrix); + } else { + _transformationController.value = matrix; + } + } + void _onReorder(SpaceReorderDataModel data, int newIndex) { final newCommunity = widget.community.copyWith(); final children = data.parent?.children ?? newCommunity.spaces; 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 index d7403588..b2a41274 100644 --- 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 @@ -7,6 +7,7 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain 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/duplicate_space/presentation/views/duplicate_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 { @@ -44,7 +45,22 @@ class CommunityStructureHeaderActionButtonsComposer extends StatelessWidget { }, ), ), - onDuplicate: (space) {}, + onDuplicate: (space) => showDialog( + context: context, + builder: (_) => DuplicateSpaceDialog( + initialName: space.spaceName, + selectedSpaceUuid: space.uuid, + selectedCommunityUuid: selectedCommunity.uuid, + onSuccess: (spaces) { + final updatedCommunity = selectedCommunity.copyWith( + spaces: spaces, + ); + context.read().add( + CommunitiesUpdateCommunity(updatedCommunity), + ); + }, + ), + ), onEdit: (space) => SpaceDetailsDialogHelper.showEdit( context, spaceModel: selectedSpace!, diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart index 5d81bffb..6e43b4e7 100644 --- a/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart @@ -10,21 +10,26 @@ class SpaceManagementBody extends StatelessWidget { @override Widget build(BuildContext context) { - return Row( + return Stack( children: [ - const SpaceManagementCommunitiesTree(), - Expanded( - child: BlocBuilder( - buildWhen: (previous, current) => - previous.selectedCommunity != current.selectedCommunity, - builder: (context, state) => Visibility( - visible: state.selectedCommunity == null, - replacement: const SpaceManagementCommunityStructure(), - child: const SpaceManagementTemplatesView(), + Row( + children: [ + const SizedBox(width: 320), + Expanded( + child: BlocBuilder( + buildWhen: (previous, current) => + previous.selectedCommunity != current.selectedCommunity, + builder: (context, state) => Visibility( + visible: state.selectedCommunity == null, + replacement: const SpaceManagementCommunityStructure(), + child: const SpaceManagementTemplatesView(), + ), + ), ), - ), + ], ), + const SpaceManagementCommunitiesTree(), ], ); } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart index 1adf9911..d986ef01 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart @@ -9,6 +9,7 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/presen import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_community_tile.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/style.dart'; class SpaceManagementCommunitiesTree extends StatefulWidget { @@ -44,7 +45,15 @@ class _SpaceManagementCommunitiesTreeState return BlocBuilder( builder: (context, state) => Container( width: 320, - decoration: subSectionContainerDecoration, + decoration: subSectionContainerDecoration.copyWith( + boxShadow: [ + BoxShadow( + color: ColorsManager.shadowBlackColor.withValues(alpha: 0.1), + blurRadius: 10, + offset: const Offset(10, 0), + ), + ], + ), child: Column( children: [ const SpaceManagementSidebarHeader(), diff --git a/lib/pages/space_management_v2/modules/duplicate_space/data/services/remote_duplicate_space_service.dart b/lib/pages/space_management_v2/modules/duplicate_space/data/services/remote_duplicate_space_service.dart new file mode 100644 index 00000000..7a13d4eb --- /dev/null +++ b/lib/pages/space_management_v2/modules/duplicate_space/data/services/remote_duplicate_space_service.dart @@ -0,0 +1,60 @@ +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/duplicate_space/domain/params/duplicate_space_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/duplicate_space/domain/services/duplicate_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 RemoteDuplicateSpaceService implements DuplicateSpaceService { + RemoteDuplicateSpaceService(this._httpService); + + final HTTPService _httpService; + + @override + Future> duplicateSpace(DuplicateSpaceParam param) async { + try { + final response = await _httpService.post( + path: await _makeUrl(param), + body: param.toJson(), + expectedResponseModel: (json) { + final response = json as Map; + final data = response['data'] as List; + return data + .map((e) => SpaceModel.fromJson(e as Map)) + .toList(); + }, + ); + + 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? ?? ''; + throw APIException(errorMessage); + } catch (e) { + throw APIException(e.toString()); + } + } + + Future _makeUrl(DuplicateSpaceParam 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.duplicateSpace + .replaceAll('{projectUuid}', projectUuid) + .replaceAll('{communityUuid}', param.communityUuid) + .replaceAll('{spaceUuid}', param.spaceUuid); + } +} diff --git a/lib/pages/space_management_v2/modules/duplicate_space/domain/params/duplicate_space_param.dart b/lib/pages/space_management_v2/modules/duplicate_space/domain/params/duplicate_space_param.dart new file mode 100644 index 00000000..4f955ab5 --- /dev/null +++ b/lib/pages/space_management_v2/modules/duplicate_space/domain/params/duplicate_space_param.dart @@ -0,0 +1,15 @@ +class DuplicateSpaceParam { + final String communityUuid; + final String spaceUuid; + final String newSpaceName; + + DuplicateSpaceParam({ + required this.communityUuid, + required this.spaceUuid, + required this.newSpaceName, + }); + + Map toJson() => { + 'spaceName': newSpaceName, + }; +} diff --git a/lib/pages/space_management_v2/modules/duplicate_space/domain/services/duplicate_space_service.dart b/lib/pages/space_management_v2/modules/duplicate_space/domain/services/duplicate_space_service.dart new file mode 100644 index 00000000..56f0d961 --- /dev/null +++ b/lib/pages/space_management_v2/modules/duplicate_space/domain/services/duplicate_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/duplicate_space/domain/params/duplicate_space_param.dart'; + +abstract interface class DuplicateSpaceService { + Future> duplicateSpace(DuplicateSpaceParam param); +} diff --git a/lib/pages/space_management_v2/modules/duplicate_space/presentation/bloc/duplicate_space_bloc.dart b/lib/pages/space_management_v2/modules/duplicate_space/presentation/bloc/duplicate_space_bloc.dart new file mode 100644 index 00000000..482bf399 --- /dev/null +++ b/lib/pages/space_management_v2/modules/duplicate_space/presentation/bloc/duplicate_space_bloc.dart @@ -0,0 +1,36 @@ +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/duplicate_space/domain/params/duplicate_space_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/duplicate_space/domain/services/duplicate_space_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; + +part 'duplicate_space_event.dart'; +part 'duplicate_space_state.dart'; + +class DuplicateSpaceBloc extends Bloc { + DuplicateSpaceBloc( + this._duplicateSpaceService, + ) : super(const DuplicateSpaceInitial()) { + on(_onDuplicateSpaceEvent); + } + + final DuplicateSpaceService _duplicateSpaceService; + + Future _onDuplicateSpaceEvent( + DuplicateSpaceEvent event, + Emitter emit, + ) async { + try { + emit(const DuplicateSpaceLoading()); + final result = await _duplicateSpaceService.duplicateSpace(event.param); + emit(DuplicateSpaceSuccess(result)); + } on APIException catch (e) { + emit(DuplicateSpaceFailure(e.message)); + } catch (e) { + emit(DuplicateSpaceFailure(e.toString())); + } finally { + emit(const DuplicateSpaceInitial()); + } + } +} diff --git a/lib/pages/space_management_v2/modules/duplicate_space/presentation/bloc/duplicate_space_event.dart b/lib/pages/space_management_v2/modules/duplicate_space/presentation/bloc/duplicate_space_event.dart new file mode 100644 index 00000000..5a437831 --- /dev/null +++ b/lib/pages/space_management_v2/modules/duplicate_space/presentation/bloc/duplicate_space_event.dart @@ -0,0 +1,10 @@ +part of 'duplicate_space_bloc.dart'; + +final class DuplicateSpaceEvent extends Equatable { + const DuplicateSpaceEvent({required this.param}); + + final DuplicateSpaceParam param; + + @override + List get props => [param]; +} diff --git a/lib/pages/space_management_v2/modules/duplicate_space/presentation/bloc/duplicate_space_state.dart b/lib/pages/space_management_v2/modules/duplicate_space/presentation/bloc/duplicate_space_state.dart new file mode 100644 index 00000000..d104b184 --- /dev/null +++ b/lib/pages/space_management_v2/modules/duplicate_space/presentation/bloc/duplicate_space_state.dart @@ -0,0 +1,34 @@ +part of 'duplicate_space_bloc.dart'; + +sealed class DuplicateSpaceState extends Equatable { + const DuplicateSpaceState(); + + @override + List get props => []; +} + +final class DuplicateSpaceInitial extends DuplicateSpaceState { + const DuplicateSpaceInitial(); +} + +final class DuplicateSpaceLoading extends DuplicateSpaceState { + const DuplicateSpaceLoading(); +} + +final class DuplicateSpaceSuccess extends DuplicateSpaceState { + const DuplicateSpaceSuccess(this.spaces); + + final List spaces; + + @override + List get props => [spaces]; +} + +final class DuplicateSpaceFailure extends DuplicateSpaceState { + const DuplicateSpaceFailure(this.errorMessage); + + final String errorMessage; + + @override + List get props => [errorMessage]; +} diff --git a/lib/pages/space_management_v2/modules/duplicate_space/presentation/views/duplicate_space_dialog.dart b/lib/pages/space_management_v2/modules/duplicate_space/presentation/views/duplicate_space_dialog.dart new file mode 100644 index 00000000..dcda4a58 --- /dev/null +++ b/lib/pages/space_management_v2/modules/duplicate_space/presentation/views/duplicate_space_dialog.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/common/widgets/app_loading_indicator.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/duplicate_space/data/services/remote_duplicate_space_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/duplicate_space/presentation/bloc/duplicate_space_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/duplicate_space/presentation/widgets/duplicate_space_dialog_form.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/extension/app_snack_bar.dart'; + +class DuplicateSpaceDialog extends StatelessWidget { + const DuplicateSpaceDialog({ + required this.initialName, + required this.onSuccess, + required this.selectedSpaceUuid, + required this.selectedCommunityUuid, + super.key, + }); + + final String initialName; + final void Function(List spaces) onSuccess; + final String selectedSpaceUuid; + final String selectedCommunityUuid; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => DuplicateSpaceBloc( + RemoteDuplicateSpaceService(HTTPService()), + ), + child: BlocListener( + listener: _listener, + child: DuplicateSpaceDialogForm( + initialName: initialName, + selectedSpaceUuid: selectedSpaceUuid, + selectedCommunityUuid: selectedCommunityUuid, + ), + ), + ); + } + + void _listener(BuildContext context, DuplicateSpaceState state) { + switch (state) { + case DuplicateSpaceLoading(): + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const AppLoadingIndicator(), + ); + break; + + case DuplicateSpaceFailure(:final errorMessage): + Navigator.pop(context); + Navigator.pop(context); + context.showFailureSnackbar(errorMessage); + break; + + case DuplicateSpaceSuccess(:final spaces): + onSuccess.call(spaces); + Navigator.of(context).pop(); + Navigator.of(context).pop(); + context.showSuccessSnackbar('Space duplicated successfully'); + break; + + default: + break; + } + } +} diff --git a/lib/pages/space_management_v2/modules/duplicate_space/presentation/widgets/duplicate_space_dialog_form.dart b/lib/pages/space_management_v2/modules/duplicate_space/presentation/widgets/duplicate_space_dialog_form.dart new file mode 100644 index 00000000..7f4ec966 --- /dev/null +++ b/lib/pages/space_management_v2/modules/duplicate_space/presentation/widgets/duplicate_space_dialog_form.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/duplicate_space/domain/params/duplicate_space_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/duplicate_space/presentation/bloc/duplicate_space_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/duplicate_space/presentation/widgets/duplicate_space_text_field.dart'; + +class DuplicateSpaceDialogForm extends StatefulWidget { + const DuplicateSpaceDialogForm({ + required this.initialName, + required this.selectedSpaceUuid, + required this.selectedCommunityUuid, + super.key, + }); + + final String initialName; + final String selectedSpaceUuid; + final String selectedCommunityUuid; + + @override + State createState() => _DuplicateSpaceDialogFormState(); +} + +class _DuplicateSpaceDialogFormState extends State { + late final TextEditingController _nameController; + bool _isNameValid = true; + + @override + void initState() { + super.initState(); + _nameController = TextEditingController(text: '${widget.initialName}(1)'); + _nameController.addListener(_validateName); + } + + void _validateName() => setState( + () => _isNameValid = _nameController.text.trim() != widget.initialName, + ); + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const SelectableText('Duplicate Space'), + content: Column( + mainAxisSize: MainAxisSize.min, + spacing: 16, + children: [ + const SelectableText('Enter a new name for the duplicated space:'), + DuplicateSpaceTextField( + nameController: _nameController, + isNameValid: _isNameValid, + initialName: widget.initialName, + ), + ], + ), + actions: [ + TextButton( + onPressed: Navigator.of(context).pop, + child: const Text('Cancel'), + ), + TextButton( + onPressed: _isNameValid ? () => _submit(context) : null, + child: const Text('Duplicate'), + ), + ], + ); + } + + void _submit(BuildContext context) { + context.read().add( + DuplicateSpaceEvent( + param: DuplicateSpaceParam( + newSpaceName: _nameController.text, + spaceUuid: widget.selectedSpaceUuid, + communityUuid: widget.selectedCommunityUuid, + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/duplicate_space/presentation/widgets/duplicate_space_failure_dialog.dart b/lib/pages/space_management_v2/modules/duplicate_space/presentation/widgets/duplicate_space_failure_dialog.dart new file mode 100644 index 00000000..3aaa7b3d --- /dev/null +++ b/lib/pages/space_management_v2/modules/duplicate_space/presentation/widgets/duplicate_space_failure_dialog.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +class DuplicateSpaceFailureDialog extends StatelessWidget { + const DuplicateSpaceFailureDialog(this.errorMessage, {super.key}); + + final String errorMessage; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Failed to duplicate space'), + content: Text(errorMessage), + actions: [ + TextButton( + onPressed: Navigator.of(context).pop, + child: const Text('Close'), + ), + ], + ); + } +} diff --git a/lib/pages/space_management_v2/modules/duplicate_space/presentation/widgets/duplicate_space_text_field.dart b/lib/pages/space_management_v2/modules/duplicate_space/presentation/widgets/duplicate_space_text_field.dart new file mode 100644 index 00000000..19b0201a --- /dev/null +++ b/lib/pages/space_management_v2/modules/duplicate_space/presentation/widgets/duplicate_space_text_field.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class DuplicateSpaceTextField extends StatelessWidget { + const DuplicateSpaceTextField({ + required this.nameController, + required this.isNameValid, + required this.initialName, + super.key, + }); + + final TextEditingController nameController; + final bool isNameValid; + final String initialName; + + String get _errorText => 'Name must be different from "$initialName"'; + + @override + Widget build(BuildContext context) { + return TextField( + controller: nameController, + style: context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.blackColor, + ), + decoration: InputDecoration( + label: const Text('Space Name'), + border: _border(), + enabledBorder: _border(), + focusedBorder: _border(ColorsManager.primaryColor), + errorBorder: _border(context.theme.colorScheme.error), + focusedErrorBorder: _border(context.theme.colorScheme.error), + errorStyle: context.textTheme.bodyMedium!.copyWith( + color: context.theme.colorScheme.error, + fontSize: 8, + ), + errorText: isNameValid ? null : _errorText, + ), + ); + } + + OutlineInputBorder _border([Color? color]) { + return OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide( + color: color ?? ColorsManager.blackColor, + width: 0.5, + ), + ); + } +} 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 587c9ea7..6b05ab8a 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 @@ -19,6 +19,7 @@ class SpaceSubSpacesDialog extends StatefulWidget { } class _SpaceSubSpacesDialogState extends State { + late final TextEditingController _subspaceNameController; late List _subspaces; bool get _hasDuplicateNames => @@ -29,6 +30,13 @@ class _SpaceSubSpacesDialogState extends State { void initState() { super.initState(); _subspaces = List.from(widget.subspaces); + _subspaceNameController = TextEditingController(); + } + + @override + void dispose() { + _subspaceNameController.dispose(); + super.dispose(); } void _handleSubspaceAdded(String name) { @@ -49,6 +57,10 @@ class _SpaceSubSpacesDialogState extends State { ); void _handleSave() { + final name = _subspaceNameController.text.trim(); + if (name.isNotEmpty) { + _handleSubspaceAdded(name); + } widget.onSave(_subspaces); Navigator.of(context).pop(); } @@ -65,6 +77,7 @@ class _SpaceSubSpacesDialogState extends State { subSpaces: _subspaces, onSubspaceAdded: _handleSubspaceAdded, onSubspaceDeleted: _handleSubspaceDeleted, + controller: _subspaceNameController, ), AnimatedSwitcher( duration: const Duration(milliseconds: 100), 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 591f741c..dac52f93 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 @@ -10,29 +10,28 @@ class SubSpacesInput extends StatefulWidget { required this.subSpaces, required this.onSubspaceAdded, required this.onSubspaceDeleted, + required this.controller, }); final List subSpaces; final void Function(String name) onSubspaceAdded; final void Function(String uuid) onSubspaceDeleted; + final TextEditingController controller; @override State createState() => _SubSpacesInputState(); } class _SubSpacesInputState extends State { - late final TextEditingController _subspaceNameController; late final FocusNode _focusNode; @override void initState() { super.initState(); - _subspaceNameController = TextEditingController(); _focusNode = FocusNode(); } @override void dispose() { - _subspaceNameController.dispose(); _focusNode.dispose(); super.dispose(); } @@ -81,7 +80,7 @@ class _SubSpacesInputState extends State { width: 200, child: TextField( focusNode: _focusNode, - controller: _subspaceNameController, + controller: widget.controller, decoration: InputDecoration( border: InputBorder.none, hintText: widget.subSpaces.isEmpty ? 'Please enter the name' : null, @@ -93,7 +92,7 @@ class _SubSpacesInputState extends State { final trimmedValue = value.trim(); if (trimmedValue.isNotEmpty) { widget.onSubspaceAdded(trimmedValue); - _subspaceNameController.clear(); + widget.controller.clear(); _focusNode.requestFocus(); } }, diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index cf7ffbe5..bbf8cccd 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -141,7 +141,7 @@ abstract class ApiEndpoints { '/projects/{projectUuid}/communities/{communityUuid}/spaces/{spaceUuid}/subspaces/{subSpaceUuid}/devices/{deviceUuid}'; static const String saveSchedule = '/schedule/{deviceUuid}'; - + static const String duplicateSpace = '/projects/{projectUuid}/communities/{communityUuid}/spaces/{spaceUuid}/duplicate'; ////booking System static const String bookableSpaces = '/bookable-spaces'; diff --git a/lib/utils/extension/app_snack_bar.dart b/lib/utils/extension/app_snack_bar.dart new file mode 100644 index 00000000..14cb9b43 --- /dev/null +++ b/lib/utils/extension/app_snack_bar.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +extension AppSnackBarsBuildContextExtension on BuildContext { + void showSuccessSnackbar(String message) { + ScaffoldMessenger.of(this).showSnackBar( + _makeSnackbar( + message: message, + icon: Icons.check_circle, + backgroundColor: Colors.green, + ), + ); + } + + void showFailureSnackbar(String message) { + ScaffoldMessenger.of(this).showSnackBar( + _makeSnackbar( + message: message, + icon: Icons.error, + backgroundColor: Colors.red, + ), + ); + } + + SnackBar _makeSnackbar({ + required String message, + required Color backgroundColor, + required IconData icon, + }) { + return SnackBar( + content: Row( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + Icon(icon, color: Colors.white), + Text( + message, + style: textTheme.bodyMedium?.copyWith( + color: ColorsManager.whiteColors, + ), + ), + ], + ), + backgroundColor: backgroundColor, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + margin: const EdgeInsetsDirectional.symmetric( + horizontal: 92, + vertical: 32, + ), + ); + } +}