mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-08-25 07:32:28 +00:00
Sp 1722 fe implement duplicate space feature (#365)
<!-- Thanks for contributing! Provide a description of your changes below and a general summary in the title Please look at the following checklist to ensure that your PR can be accepted quickly: --> ## Jira Ticket [SP-1722](https://syncrow.atlassian.net/browse/SP-1722) ## Description Implemented a feature that allows users to duplicate a space. ## Type of Change <!--- Put an `x` in all the boxes that apply: --> - [x] ✨ New feature (non-breaking change which adds functionality) - [ ] 🛠️ Bug fix (non-breaking change which fixes an issue) - [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change) - [ ] 🧹 Code refactor - [ ] ✅ Build configuration change - [ ] 📝 Documentation - [ ] 🗑️ Chore [SP-1722]: https://syncrow.atlassian.net/browse/SP-1722?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
This commit is contained in:
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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/helpers/spaces_recursive_helper.dart';
|
||||||
@ -51,13 +53,23 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
|||||||
duration: const Duration(milliseconds: 150),
|
duration: const Duration(milliseconds: 150),
|
||||||
);
|
);
|
||||||
super.initState();
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) {
|
||||||
|
_centerOnTree();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(covariant CommunityStructureCanvas oldWidget) {
|
void didUpdateWidget(covariant CommunityStructureCanvas oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (widget.selectedSpace == null) return;
|
if (oldWidget.community.uuid != widget.community.uuid) {
|
||||||
if (widget.selectedSpace?.uuid != oldWidget.selectedSpace?.uuid) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) {
|
||||||
|
_centerOnTree(animate: true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (widget.selectedSpace?.uuid != oldWidget.selectedSpace?.uuid) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
_animateToSpace(widget.selectedSpace);
|
_animateToSpace(widget.selectedSpace);
|
||||||
@ -151,6 +163,60 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
|||||||
_runAnimation(matrix);
|
_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) {
|
void _onReorder(SpaceReorderDataModel data, int newIndex) {
|
||||||
final newCommunity = widget.community.copyWith();
|
final newCommunity = widget.community.copyWith();
|
||||||
final children = data.parent?.children ?? newCommunity.spaces;
|
final children = data.parent?.children ?? newCommunity.spaces;
|
||||||
|
@ -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/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/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/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';
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
|
||||||
|
|
||||||
class CommunityStructureHeaderActionButtonsComposer extends StatelessWidget {
|
class CommunityStructureHeaderActionButtonsComposer extends StatelessWidget {
|
||||||
@ -44,7 +45,22 @@ class CommunityStructureHeaderActionButtonsComposer extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onDuplicate: (space) {},
|
onDuplicate: (space) => showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => DuplicateSpaceDialog(
|
||||||
|
initialName: space.spaceName,
|
||||||
|
selectedSpaceUuid: space.uuid,
|
||||||
|
selectedCommunityUuid: selectedCommunity.uuid,
|
||||||
|
onSuccess: (spaces) {
|
||||||
|
final updatedCommunity = selectedCommunity.copyWith(
|
||||||
|
spaces: spaces,
|
||||||
|
);
|
||||||
|
context.read<CommunitiesBloc>().add(
|
||||||
|
CommunitiesUpdateCommunity(updatedCommunity),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
onEdit: (space) => SpaceDetailsDialogHelper.showEdit(
|
onEdit: (space) => SpaceDetailsDialogHelper.showEdit(
|
||||||
context,
|
context,
|
||||||
spaceModel: selectedSpace!,
|
spaceModel: selectedSpace!,
|
||||||
|
@ -10,21 +10,26 @@ class SpaceManagementBody extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Row(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
const SpaceManagementCommunitiesTree(),
|
Row(
|
||||||
Expanded(
|
children: [
|
||||||
child: BlocBuilder<CommunitiesTreeSelectionBloc,
|
const SizedBox(width: 320),
|
||||||
CommunitiesTreeSelectionState>(
|
Expanded(
|
||||||
buildWhen: (previous, current) =>
|
child: BlocBuilder<CommunitiesTreeSelectionBloc,
|
||||||
previous.selectedCommunity != current.selectedCommunity,
|
CommunitiesTreeSelectionState>(
|
||||||
builder: (context, state) => Visibility(
|
buildWhen: (previous, current) =>
|
||||||
visible: state.selectedCommunity == null,
|
previous.selectedCommunity != current.selectedCommunity,
|
||||||
replacement: const SpaceManagementCommunityStructure(),
|
builder: (context, state) => Visibility(
|
||||||
child: const SpaceManagementTemplatesView(),
|
visible: state.selectedCommunity == null,
|
||||||
|
replacement: const SpaceManagementCommunityStructure(),
|
||||||
|
child: const SpaceManagementTemplatesView(),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
|
const SpaceManagementCommunitiesTree(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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_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_communities_list.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.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';
|
import 'package:syncrow_web/utils/style.dart';
|
||||||
|
|
||||||
class SpaceManagementCommunitiesTree extends StatefulWidget {
|
class SpaceManagementCommunitiesTree extends StatefulWidget {
|
||||||
@ -44,7 +45,15 @@ class _SpaceManagementCommunitiesTreeState
|
|||||||
return BlocBuilder<CommunitiesBloc, CommunitiesState>(
|
return BlocBuilder<CommunitiesBloc, CommunitiesState>(
|
||||||
builder: (context, state) => Container(
|
builder: (context, state) => Container(
|
||||||
width: 320,
|
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(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const SpaceManagementSidebarHeader(),
|
const SpaceManagementSidebarHeader(),
|
||||||
|
@ -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<List<SpaceModel>> duplicateSpace(DuplicateSpaceParam param) async {
|
||||||
|
try {
|
||||||
|
final response = await _httpService.post(
|
||||||
|
path: await _makeUrl(param),
|
||||||
|
body: param.toJson(),
|
||||||
|
expectedResponseModel: (json) {
|
||||||
|
final response = json as Map<String, dynamic>;
|
||||||
|
final data = response['data'] as List<dynamic>;
|
||||||
|
return data
|
||||||
|
.map((e) => SpaceModel.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} on DioException catch (e) {
|
||||||
|
final message = e.response?.data as Map<String, dynamic>?;
|
||||||
|
final error = message?['error'] as Map<String, dynamic>?;
|
||||||
|
final errorMessage = error?['error'] as String? ?? '';
|
||||||
|
throw APIException(errorMessage);
|
||||||
|
} catch (e) {
|
||||||
|
throw APIException(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _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);
|
||||||
|
}
|
||||||
|
}
|
@ -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<String, dynamic> toJson() => {
|
||||||
|
'spaceName': newSpaceName,
|
||||||
|
};
|
||||||
|
}
|
@ -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<List<SpaceModel>> duplicateSpace(DuplicateSpaceParam param);
|
||||||
|
}
|
@ -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<DuplicateSpaceEvent, DuplicateSpaceState> {
|
||||||
|
DuplicateSpaceBloc(
|
||||||
|
this._duplicateSpaceService,
|
||||||
|
) : super(const DuplicateSpaceInitial()) {
|
||||||
|
on<DuplicateSpaceEvent>(_onDuplicateSpaceEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
final DuplicateSpaceService _duplicateSpaceService;
|
||||||
|
|
||||||
|
Future<void> _onDuplicateSpaceEvent(
|
||||||
|
DuplicateSpaceEvent event,
|
||||||
|
Emitter<DuplicateSpaceState> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<Object> get props => [param];
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
part of 'duplicate_space_bloc.dart';
|
||||||
|
|
||||||
|
sealed class DuplicateSpaceState extends Equatable {
|
||||||
|
const DuplicateSpaceState();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> 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<SpaceModel> spaces;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [spaces];
|
||||||
|
}
|
||||||
|
|
||||||
|
final class DuplicateSpaceFailure extends DuplicateSpaceState {
|
||||||
|
const DuplicateSpaceFailure(this.errorMessage);
|
||||||
|
|
||||||
|
final String errorMessage;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [errorMessage];
|
||||||
|
}
|
@ -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<SpaceModel> spaces) onSuccess;
|
||||||
|
final String selectedSpaceUuid;
|
||||||
|
final String selectedCommunityUuid;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) => DuplicateSpaceBloc(
|
||||||
|
RemoteDuplicateSpaceService(HTTPService()),
|
||||||
|
),
|
||||||
|
child: BlocListener<DuplicateSpaceBloc, DuplicateSpaceState>(
|
||||||
|
listener: _listener,
|
||||||
|
child: DuplicateSpaceDialogForm(
|
||||||
|
initialName: initialName,
|
||||||
|
selectedSpaceUuid: selectedSpaceUuid,
|
||||||
|
selectedCommunityUuid: selectedCommunityUuid,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _listener(BuildContext context, DuplicateSpaceState state) {
|
||||||
|
switch (state) {
|
||||||
|
case DuplicateSpaceLoading():
|
||||||
|
showDialog<void>(
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<DuplicateSpaceDialogForm> createState() => _DuplicateSpaceDialogFormState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DuplicateSpaceDialogFormState extends State<DuplicateSpaceDialogForm> {
|
||||||
|
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<DuplicateSpaceBloc>().add(
|
||||||
|
DuplicateSpaceEvent(
|
||||||
|
param: DuplicateSpaceParam(
|
||||||
|
newSpaceName: _nameController.text,
|
||||||
|
spaceUuid: widget.selectedSpaceUuid,
|
||||||
|
communityUuid: widget.selectedCommunityUuid,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,7 @@ class SpaceSubSpacesDialog extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SpaceSubSpacesDialogState extends State<SpaceSubSpacesDialog> {
|
class _SpaceSubSpacesDialogState extends State<SpaceSubSpacesDialog> {
|
||||||
|
late final TextEditingController _subspaceNameController;
|
||||||
late List<Subspace> _subspaces;
|
late List<Subspace> _subspaces;
|
||||||
|
|
||||||
bool get _hasDuplicateNames =>
|
bool get _hasDuplicateNames =>
|
||||||
@ -29,6 +30,13 @@ class _SpaceSubSpacesDialogState extends State<SpaceSubSpacesDialog> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_subspaces = List.from(widget.subspaces);
|
_subspaces = List.from(widget.subspaces);
|
||||||
|
_subspaceNameController = TextEditingController();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_subspaceNameController.dispose();
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleSubspaceAdded(String name) {
|
void _handleSubspaceAdded(String name) {
|
||||||
@ -49,6 +57,10 @@ class _SpaceSubSpacesDialogState extends State<SpaceSubSpacesDialog> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
void _handleSave() {
|
void _handleSave() {
|
||||||
|
final name = _subspaceNameController.text.trim();
|
||||||
|
if (name.isNotEmpty) {
|
||||||
|
_handleSubspaceAdded(name);
|
||||||
|
}
|
||||||
widget.onSave(_subspaces);
|
widget.onSave(_subspaces);
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
@ -65,6 +77,7 @@ class _SpaceSubSpacesDialogState extends State<SpaceSubSpacesDialog> {
|
|||||||
subSpaces: _subspaces,
|
subSpaces: _subspaces,
|
||||||
onSubspaceAdded: _handleSubspaceAdded,
|
onSubspaceAdded: _handleSubspaceAdded,
|
||||||
onSubspaceDeleted: _handleSubspaceDeleted,
|
onSubspaceDeleted: _handleSubspaceDeleted,
|
||||||
|
controller: _subspaceNameController,
|
||||||
),
|
),
|
||||||
AnimatedSwitcher(
|
AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 100),
|
duration: const Duration(milliseconds: 100),
|
||||||
|
@ -10,29 +10,28 @@ class SubSpacesInput extends StatefulWidget {
|
|||||||
required this.subSpaces,
|
required this.subSpaces,
|
||||||
required this.onSubspaceAdded,
|
required this.onSubspaceAdded,
|
||||||
required this.onSubspaceDeleted,
|
required this.onSubspaceDeleted,
|
||||||
|
required this.controller,
|
||||||
});
|
});
|
||||||
|
|
||||||
final List<Subspace> subSpaces;
|
final List<Subspace> subSpaces;
|
||||||
final void Function(String name) onSubspaceAdded;
|
final void Function(String name) onSubspaceAdded;
|
||||||
final void Function(String uuid) onSubspaceDeleted;
|
final void Function(String uuid) onSubspaceDeleted;
|
||||||
|
final TextEditingController controller;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SubSpacesInput> createState() => _SubSpacesInputState();
|
State<SubSpacesInput> createState() => _SubSpacesInputState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SubSpacesInputState extends State<SubSpacesInput> {
|
class _SubSpacesInputState extends State<SubSpacesInput> {
|
||||||
late final TextEditingController _subspaceNameController;
|
|
||||||
late final FocusNode _focusNode;
|
late final FocusNode _focusNode;
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_subspaceNameController = TextEditingController();
|
|
||||||
_focusNode = FocusNode();
|
_focusNode = FocusNode();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_subspaceNameController.dispose();
|
|
||||||
_focusNode.dispose();
|
_focusNode.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@ -81,7 +80,7 @@ class _SubSpacesInputState extends State<SubSpacesInput> {
|
|||||||
width: 200,
|
width: 200,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
controller: _subspaceNameController,
|
controller: widget.controller,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
hintText: widget.subSpaces.isEmpty ? 'Please enter the name' : null,
|
hintText: widget.subSpaces.isEmpty ? 'Please enter the name' : null,
|
||||||
@ -93,7 +92,7 @@ class _SubSpacesInputState extends State<SubSpacesInput> {
|
|||||||
final trimmedValue = value.trim();
|
final trimmedValue = value.trim();
|
||||||
if (trimmedValue.isNotEmpty) {
|
if (trimmedValue.isNotEmpty) {
|
||||||
widget.onSubspaceAdded(trimmedValue);
|
widget.onSubspaceAdded(trimmedValue);
|
||||||
_subspaceNameController.clear();
|
widget.controller.clear();
|
||||||
_focusNode.requestFocus();
|
_focusNode.requestFocus();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -141,7 +141,7 @@ abstract class ApiEndpoints {
|
|||||||
'/projects/{projectUuid}/communities/{communityUuid}/spaces/{spaceUuid}/subspaces/{subSpaceUuid}/devices/{deviceUuid}';
|
'/projects/{projectUuid}/communities/{communityUuid}/spaces/{spaceUuid}/subspaces/{subSpaceUuid}/devices/{deviceUuid}';
|
||||||
static const String saveSchedule = '/schedule/{deviceUuid}';
|
static const String saveSchedule = '/schedule/{deviceUuid}';
|
||||||
|
|
||||||
|
static const String duplicateSpace = '/projects/{projectUuid}/communities/{communityUuid}/spaces/{spaceUuid}/duplicate';
|
||||||
|
|
||||||
////booking System
|
////booking System
|
||||||
static const String bookableSpaces = '/bookable-spaces';
|
static const String bookableSpaces = '/bookable-spaces';
|
||||||
|
56
lib/utils/extension/app_snack_bar.dart
Normal file
56
lib/utils/extension/app_snack_bar.dart
Normal file
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user