Compare commits

...
Author SHA1 Message Date
Faris Armoush ae3eb6fca8 Merge branch 'dev' of https://github.com/SyncrowIOT/web into SP-1722-FE-Implement-Duplicate-Space-Feature 2025-07-24 09:52:06 +03:00
Faris Armoush f341dcd482 Remove unnecessary event dispatch in CommunityStructureHeaderActionButtonsComposer to streamline community update logic. This change enhances code clarity by eliminating the selection event for the community, focusing solely on the update action. 2025-07-24 09:47:39 +03:00
Faris Armoush e98b091253 Refactor SpaceManagementBody to use a Stack layout for improved UI structure, allowing better positioning of the SpaceManagementCommunitiesTree and the main content. Enhance SpaceManagementCommunitiesTree with a shadow effect for better visual separation. This change promotes a more organized and visually appealing interface. 2025-07-24 09:39:38 +03:00
Faris Armoush 77d6d822cb Refactor SpaceSubSpacesDialog and SubSpacesInput to integrate a shared TextEditingController for improved state management of subspace names. This change enhances the input handling and ensures proper disposal of the controller, promoting better resource management. 2025-07-24 09:39:33 +03:00
mohammadnemer1 f1aab13263 refactor booking page to use PageController (#366)
<!--
  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:
-->

## Description

<!--- Describe your changes in detail -->
refactor booking page to use PageController 

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ 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
2025-07-23 14:37:43 +03:00
Faris Armoush 97530dd351 Merge branch 'dev' of https://github.com/SyncrowIOT/web into SP-1722-FE-Implement-Duplicate-Space-Feature 2025-07-23 14:19:55 +03:00
Faris Armoush a57b6e0853 Enhance CommunityStructureCanvas by adding a _centerOnTree method to improve the centering logic of the community structure. This method calculates the optimal view based on the positions of spaces and adjusts the transformation controller accordingly, ensuring a smoother user experience during updates and animations. 2025-07-23 14:19:25 +03:00
Faris Armoush 845397e819 Update CommunityStructureCanvas to improve widget update logic by animating to the selected space based on community UUID changes. This enhances the responsiveness of the UI when the community context changes. 2025-07-23 13:21:49 +03:00
Faris Armoush 2077ef053f Refactor DuplicateSpaceService to return a list of SpaceModel objects instead of a single instance. Update related components including DuplicateSpaceSuccess state and DuplicateSpaceDialog to handle multiple spaces. Enhance CommunityStructureHeaderActionButtonsComposer to reflect these changes in the success callback. 2025-07-23 12:42:23 +03:00
Faris Armoush d21850edc8 Enhance DuplicateSpaceDialog to use Bloc for state management and streamline the duplication process with success and error handling. Update CommunityStructureHeaderActionButtonsComposer to integrate the new dialog for duplicating spaces. 2025-07-23 12:37:27 +03:00
Faris Armoush 85f53ed1f2 Remove DuplicateSpaceBloc and its associated service from SpaceManagementPage to streamline dependencies and improve code clarity. 2025-07-23 12:37:17 +03:00
Faris Armoush 5fde74fc7d Add DuplicateSpaceFailureDialog widget to display error messages when duplicating spaces fails. 2025-07-23 12:37:07 +03:00
Faris Armoush 994efc302b Add AppSnackBarsBuildContextExtension for displaying success and failure snack bars in the app. 2025-07-23 12:36:45 +03:00
Faris Armoush 04b7a506be Remove newSpaceIcon parameter from DuplicateSpaceParam class since it isnt needed. 2025-07-23 10:52:46 +03:00
Faris Armoush 19ddf443a9 Refactor RemoteDuplicateSpaceService to improve code readability by aligning method chaining for URL replacements. 2025-07-23 10:52:29 +03:00
Faris Armoush 3779176978 Add DuplicateSpaceDialog widget for user interaction in duplicate space management. 2025-07-23 10:52:18 +03:00
Faris Armoush 7c5bca35fc Add DuplicateSpaceTextField widget for user input in duplicate space management. 2025-07-23 10:52:07 +03:00
Faris Armoush aed3004a31 Added DuplicateSpaceBloc to SpaceManagementPage for managing duplicate space functionality. 2025-07-23 10:51:51 +03:00
Faris Armoush 4241d11cb6 Implemented duplicate_space data layer. 2025-07-23 09:59:13 +03:00
Faris Armoush ef8c9efff0 Added duplicate space endpoint to ApiEndpoints. 2025-07-23 09:58:46 +03:00
Faris Armoush c59d2b7fd6 Implemented toJson method in DuplicateSpaceParam. 2025-07-23 09:58:15 +03:00
Faris Armoush 71f0da9299 Created duplicate_space presentation layer. 2025-07-23 09:51:09 +03:00
Faris Armoush e6d9000ee2 Implemented duplicate space domain layer. 2025-07-23 09:48:23 +03:00
18 changed files with 572 additions and 22 deletions
@@ -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();
} }
}, },
+1 -1
View File
@@ -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
View 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,
),
);
}
}