mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-08-24 20:42:27 +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_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<CommunityStructureCanvas>
|
||||
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<CommunityStructureCanvas>
|
||||
_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;
|
||||
|
@ -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<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(
|
||||
context,
|
||||
spaceModel: selectedSpace!,
|
||||
|
@ -10,21 +10,26 @@ class SpaceManagementBody extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
return Stack(
|
||||
children: [
|
||||
const SpaceManagementCommunitiesTree(),
|
||||
Expanded(
|
||||
child: BlocBuilder<CommunitiesTreeSelectionBloc,
|
||||
CommunitiesTreeSelectionState>(
|
||||
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<CommunitiesTreeSelectionBloc,
|
||||
CommunitiesTreeSelectionState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.selectedCommunity != current.selectedCommunity,
|
||||
builder: (context, state) => Visibility(
|
||||
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_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<CommunitiesBloc, CommunitiesState>(
|
||||
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(),
|
||||
|
@ -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> {
|
||||
late final TextEditingController _subspaceNameController;
|
||||
late List<Subspace> _subspaces;
|
||||
|
||||
bool get _hasDuplicateNames =>
|
||||
@ -29,6 +30,13 @@ class _SpaceSubSpacesDialogState extends State<SpaceSubSpacesDialog> {
|
||||
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<SpaceSubSpacesDialog> {
|
||||
);
|
||||
|
||||
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<SpaceSubSpacesDialog> {
|
||||
subSpaces: _subspaces,
|
||||
onSubspaceAdded: _handleSubspaceAdded,
|
||||
onSubspaceDeleted: _handleSubspaceDeleted,
|
||||
controller: _subspaceNameController,
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
|
@ -10,29 +10,28 @@ class SubSpacesInput extends StatefulWidget {
|
||||
required this.subSpaces,
|
||||
required this.onSubspaceAdded,
|
||||
required this.onSubspaceDeleted,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
final List<Subspace> subSpaces;
|
||||
final void Function(String name) onSubspaceAdded;
|
||||
final void Function(String uuid) onSubspaceDeleted;
|
||||
final TextEditingController controller;
|
||||
|
||||
@override
|
||||
State<SubSpacesInput> createState() => _SubSpacesInputState();
|
||||
}
|
||||
|
||||
class _SubSpacesInputState extends State<SubSpacesInput> {
|
||||
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<SubSpacesInput> {
|
||||
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<SubSpacesInput> {
|
||||
final trimmedValue = value.trim();
|
||||
if (trimmedValue.isNotEmpty) {
|
||||
widget.onSubspaceAdded(trimmedValue);
|
||||
_subspaceNameController.clear();
|
||||
widget.controller.clear();
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
},
|
||||
|
@ -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';
|
||||
|
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