From 8caee328229308ca5e9664fac66b247d348f8068 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Wed, 18 Jun 2025 09:39:49 +0300 Subject: [PATCH 01/11] Initialized new `SpaceManagementPage`. --- .../views/space_management_page.dart | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 lib/pages/space_management_v2/main_module/views/space_management_page.dart diff --git a/lib/pages/space_management_v2/main_module/views/space_management_page.dart b/lib/pages/space_management_v2/main_module/views/space_management_page.dart new file mode 100644 index 00000000..03e17165 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/views/space_management_page.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; +import 'package:syncrow_web/utils/theme/responsive_text_theme.dart'; +import 'package:syncrow_web/web_layout/web_scaffold.dart'; + +class SpaceManagementPage extends StatelessWidget { + const SpaceManagementPage({super.key}); + + @override + Widget build(BuildContext context) { + return WebScaffold( + appBarTitle: Text( + 'Space Management', + style: ResponsiveTextTheme.of(context).deviceManagementTitle, + ), + enableMenuSidebar: false, + centerBody: Text( + 'Community Structure', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontWeight: FontWeight.bold, + ), + ), + rightBody: const NavigateHomeGridView(), + scaffoldBody: const Center(child: Text('Space Management')), + ); + } +} From 2f233db3326c0d3846b0b87506d1a4e3fb9a7401 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 11:04:39 +0300 Subject: [PATCH 02/11] implemented space management side bar. --- .../views/space_management_page.dart | 81 ++++++-- .../widgets/space_management_body.dart | 15 ++ .../services/remote_communities_service.dart | 32 +++- .../domain/models/space_model.dart | 16 ++ .../communities_tree_selection_bloc.dart | 47 +++++ .../communities_tree_selection_event.dart | 30 +++ .../communities_tree_selection_state.dart | 29 +++ .../presentation/widgets/community_tile.dart | 37 ++++ .../widgets/create_community_dialog.dart | 181 ++++++++++++++++++ .../space_management_communities_tree.dart | 160 ++++++++++++++++ ...nagement_sidebar_add_community_button.dart | 34 ++++ ...e_management_sidebar_communities_list.dart | 72 +++++++ .../space_management_sidebar_header.dart | 36 ++++ .../presentation/widgets/space_tile.dart | 54 ++++++ .../all_spaces/widgets/space_tile_widget.dart | 3 +- lib/utils/app_routes.dart | 2 +- 16 files changed, 808 insertions(+), 21 deletions(-) create mode 100644 lib/pages/space_management_v2/main_module/widgets/space_management_body.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart diff --git a/lib/pages/space_management_v2/main_module/views/space_management_page.dart b/lib/pages/space_management_v2/main_module/views/space_management_page.dart index 03e17165..4c3c7452 100644 --- a/lib/pages/space_management_v2/main_module/views/space_management_page.dart +++ b/lib/pages/space_management_v2/main_module/views/space_management_page.dart @@ -1,5 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_body.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.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/utils/theme/responsive_text_theme.dart'; import 'package:syncrow_web/web_layout/web_scaffold.dart'; @@ -8,20 +16,67 @@ class SpaceManagementPage extends StatelessWidget { @override Widget build(BuildContext context) { - return WebScaffold( - appBarTitle: Text( - 'Space Management', - style: ResponsiveTextTheme.of(context).deviceManagementTitle, + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => CommunitiesBloc( + communitiesService: _FakeCommunitiesService(), + )..add(const LoadCommunities(LoadCommunitiesParam())), + ), + BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()), + ], + child: WebScaffold( + appBarTitle: Text( + 'Space Management', + style: ResponsiveTextTheme.of(context).deviceManagementTitle, + ), + enableMenuSidebar: false, + centerBody: Text( + 'Community Structure', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontWeight: FontWeight.bold, + ), + ), + rightBody: const NavigateHomeGridView(), + scaffoldBody: const SpaceManagementBody(), ), - enableMenuSidebar: false, - centerBody: Text( - 'Community Structure', - style: Theme.of(context).textTheme.bodyLarge!.copyWith( - fontWeight: FontWeight.bold, - ), - ), - rightBody: const NavigateHomeGridView(), - scaffoldBody: const Center(child: Text('Space Management')), + ); + } +} + +class _FakeCommunitiesService extends CommunitiesService { + @override + Future> getCommunity(LoadCommunitiesParam param) async { + return Future.delayed( + const Duration(seconds: 1), + () => [ + const CommunityModel( + uuid: '1', + name: 'Community 1', + spaces: [ + SpaceModel( + uuid: '3', + spaceName: 'Space 1', + icon: 'assets/icons/space.png', + children: [ + SpaceModel( + uuid: '4', + spaceName: 'Space 2', + icon: 'assets/icons/space.png', + children: [], + status: SpaceStatus.active, + ), + ], + status: SpaceStatus.active, + ), + ], + ), + const CommunityModel( + uuid: '2', + name: 'Community 1', + spaces: [], + ), + ], ); } } diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart new file mode 100644 index 00000000..3a9aa3c8 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart'; + +class SpaceManagementBody extends StatelessWidget { + const SpaceManagementBody({super.key}); + + @override + Widget build(BuildContext context) { + return const Row( + children: [ + SpaceManagementCommunitiesTree(), + ], + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart index 36682bb4..83a212ca 100644 --- a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart @@ -1,9 +1,11 @@ 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/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_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'; class RemoteCommunitiesService implements CommunitiesService { const RemoteCommunitiesService(this._httpService); @@ -14,13 +16,27 @@ class RemoteCommunitiesService implements CommunitiesService { @override Future> getCommunity(LoadCommunitiesParam param) async { + final projectUuid = await ProjectManager.getProjectUUID(); + if (projectUuid == null) throw APIException('Project UUID is not set'); + try { - return _httpService.get( - path: '/api/communities/', - expectedResponseModel: (json) => (json as List) - .map((e) => CommunityModel.fromJson(e as Map)) - .toList(), + final allCommunities = []; + await _httpService.get( + path: await _makeUrl(), + expectedResponseModel: (json) { + final response = json as Map; + final jsonData = response['data'] as List? ?? []; + return jsonData + .map( + (jsonItem) => CommunityModel.fromJson( + jsonItem as Map, + ), + ) + .toList(); + }, ); + + return allCommunities; } on DioException catch (e) { final message = e.response?.data as Map?; final error = message?['error'] as Map?; @@ -31,4 +47,10 @@ class RemoteCommunitiesService implements CommunitiesService { throw APIException(formattedErrorMessage); } } + + Future _makeUrl() async { + final projectUuid = await ProjectManager.getProjectUUID(); + if (projectUuid == null) throw APIException('Project UUID is required'); + return ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectUuid); + } } diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart index 0f8aadb2..519e8ee7 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart @@ -1,16 +1,31 @@ import 'package:equatable/equatable.dart'; +enum SpaceStatus { + active, + deleted, + parentDeleted; + + static SpaceStatus getValueFromString(String value) => switch (value) { + 'active' => active, + 'deleted' => deleted, + 'parentDeleted' => parentDeleted, + _ => active, + }; +} + class SpaceModel extends Equatable { final String uuid; final String spaceName; final String icon; final List children; + final SpaceStatus status; const SpaceModel({ required this.uuid, required this.spaceName, required this.icon, required this.children, + required this.status, }); factory SpaceModel.fromJson(Map json) { @@ -22,6 +37,7 @@ class SpaceModel extends Equatable { ?.map((e) => SpaceModel.fromJson(e as Map)) .toList() ?? [], + status: SpaceStatus.getValueFromString(json['status'] as String), ); } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart new file mode 100644 index 00000000..bfc02f11 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart @@ -0,0 +1,47 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; + +part 'communities_tree_selection_event.dart'; +part 'communities_tree_selection_state.dart'; + +class CommunitiesTreeSelectionBloc + extends Bloc { + CommunitiesTreeSelectionBloc() : super(const CommunitiesTreeSelectionState()) { + on(_onSelectCommunity); + on(_onSelectSpace); + on(_onClearSelection); + } + + void _onSelectCommunity( + SelectCommunityEvent event, + Emitter emit, + ) { + emit( + CommunitiesTreeSelectionState( + selectedCommunity: event.community, + selectedSpace: null, + ), + ); + } + + void _onSelectSpace( + SelectSpaceEvent event, + Emitter emit, + ) { + emit( + CommunitiesTreeSelectionState( + selectedCommunity: null, + selectedSpace: event.space, + ), + ); + } + + void _onClearSelection( + ClearCommunitiesTreeSelectionEvent event, + Emitter emit, + ) { + emit(const CommunitiesTreeSelectionState()); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart new file mode 100644 index 00000000..95ffe173 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart @@ -0,0 +1,30 @@ +part of 'communities_tree_selection_bloc.dart'; + +sealed class CommunitiesTreeSelectionEvent extends Equatable { + const CommunitiesTreeSelectionEvent(); + + @override + List get props => []; +} + +final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent { + final CommunityModel? community; + + const SelectCommunityEvent({required this.community}); + @override + List get props => [community]; +} + +final class SelectSpaceEvent extends CommunitiesTreeSelectionEvent { + final SpaceModel? space; + + const SelectSpaceEvent({required this.space}); + + @override + List get props => [space]; +} + +final class ClearCommunitiesTreeSelectionEvent + extends CommunitiesTreeSelectionEvent { + const ClearCommunitiesTreeSelectionEvent(); +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart new file mode 100644 index 00000000..b14d330b --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart @@ -0,0 +1,29 @@ +part of 'communities_tree_selection_bloc.dart'; + +final class CommunitiesTreeSelectionState extends Equatable { + const CommunitiesTreeSelectionState({ + this.selectedCommunity, + this.selectedSpace, + }); + + final CommunityModel? selectedCommunity; + final SpaceModel? selectedSpace; + + CommunitiesTreeSelectionState copyWith({ + CommunityModel? selectedCommunity, + SpaceModel? selectedSpace, + List? expandedCommunities, + List? expandedSpaces, + }) { + return CommunitiesTreeSelectionState( + selectedCommunity: selectedCommunity ?? this.selectedCommunity, + selectedSpace: selectedSpace ?? this.selectedSpace, + ); + } + + @override + List get props => [ + selectedCommunity, + selectedSpace, + ]; + } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart new file mode 100644 index 00000000..0baaae52 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/common/widgets/custom_expansion_tile.dart'; + +class CommunityTile extends StatelessWidget { + final String title; + final List? children; + final bool isExpanded; + final bool isSelected; + final void Function(String, bool isExpanded) onExpansionChanged; + final void Function() onItemSelected; + + const CommunityTile({ + super.key, + required this.title, + required this.isExpanded, + required this.onExpansionChanged, + required this.onItemSelected, + required this.isSelected, + this.children, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: CustomExpansionTile( + title: title, + initiallyExpanded: isExpanded, + isSelected: isSelected, + onExpansionChanged: (bool expanded) { + onExpansionChanged(title, expanded); + }, + onItemSelected: onItemSelected, + children: children ?? [], + )); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart new file mode 100644 index 00000000..fd8a0a68 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart @@ -0,0 +1,181 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/spaces_management/create_community/bloc/community_dialog_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/create_community/bloc/community_dialog_event.dart'; +import 'package:syncrow_web/pages/spaces_management/create_community/bloc/community_dialog_state.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CreateCommunityDialog extends StatefulWidget { + final void Function(String name) onCreateCommunity; + final String? initialName; + final Widget title; + + const CreateCommunityDialog({ + super.key, + required this.onCreateCommunity, + required this.title, + this.initialName, + }); + + @override + State createState() => _CreateCommunityDialogState(); +} + +class _CreateCommunityDialogState extends State { + late final TextEditingController _nameController; + + @override + void initState() { + _nameController = TextEditingController(text: widget.initialName ?? ''); + super.initState(); + } + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => CommunityDialogBloc([]), + child: Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + backgroundColor: ColorsManager.transparentColor, + child: Stack( + children: [ + Container( + width: MediaQuery.of(context).size.width * 0.3, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: ColorsManager.blackColor.withOpacity(0.25), + blurRadius: 20, + spreadRadius: 5, + offset: const Offset(0, 5), + ), + ], + ), + child: SingleChildScrollView( + child: BlocBuilder( + builder: (context, state) { + var isNameValid = true; + var isNameEmpty = false; + + if (state is CommunityNameValidationState) { + isNameValid = state.isNameValid; + isNameEmpty = state.isNameEmpty; + } + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DefaultTextStyle( + style: Theme.of(context).textTheme.headlineMedium!, + child: widget.title, + ), + const SizedBox(height: 18), + TextField( + controller: _nameController, + onChanged: (value) { + context + .read() + .add(ValidateCommunityNameEvent(value)); + }, + style: Theme.of(context).textTheme.bodyMedium, + decoration: InputDecoration( + hintText: 'Please enter the community name', + filled: true, + fillColor: ColorsManager.boxColor, + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: isNameValid && !isNameEmpty + ? ColorsManager.boxColor + : ColorsManager.red, + width: 1, + ), + borderRadius: BorderRadius.circular(10), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide( + color: ColorsManager.boxColor, + width: 1.5, + ), + ), + ), + ), + if (!isNameValid) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + '*Name already exists.', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: ColorsManager.red), + ), + ), + if (isNameEmpty) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + '*Name should not be empty.', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: ColorsManager.red), + ), + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: CancelButton( + label: 'Cancel', + onPressed: () => Navigator.of(context).pop(), + ), + ), + const SizedBox(width: 16), + Expanded( + child: DefaultButton( + onPressed: () { + if (isNameValid && !isNameEmpty) { + widget.onCreateCommunity( + _nameController.text.trim(), + ); + Navigator.of(context).pop(); + } + }, + backgroundColor: isNameValid && !isNameEmpty + ? ColorsManager.secondaryColor + : ColorsManager.lightGrayColor, + borderRadius: 10, + foregroundColor: ColorsManager.whiteColors, + child: const Text('OK'), + ), + ), + ], + ), + ], + ); + }, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart new file mode 100644 index 00000000..3248fa7d --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart @@ -0,0 +1,160 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/common/widgets/search_bar.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.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_tile.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class SpaceManagementCommunitiesTree extends StatelessWidget { + const SpaceManagementCommunitiesTree({super.key}); + + bool _isSpaceOrChildSelected(BuildContext context, SpaceModel space) { + final selectedSpace = + context.read().state.selectedSpace; + final isSpaceSelected = selectedSpace?.uuid == space.uuid; + final anySubSpaceIsSelected = space.children.any( + (child) => _isSpaceOrChildSelected(context, child), + ); + return isSpaceSelected || anySubSpaceIsSelected; + } + + static const _width = 300.0; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Container( + width: _width, + decoration: subSectionContainerDecoration, + child: Column( + children: [ + SpaceManagementSidebarHeader( + onAddCommunity: () => _onAddCommunity(context), + ), + CustomSearchBar(onSearchChanged: (value) {}), + const SizedBox(height: 16), + switch (state.status) { + CommunitiesStatus.initial => + const Center(child: CircularProgressIndicator()), + CommunitiesStatus.loading => + const Center(child: CircularProgressIndicator()), + CommunitiesStatus.success => + _buildCommunitiesTree(context, state.communities), + CommunitiesStatus.failure => Center( + child: Text(state.errorMessage ?? 'Something went wrong'), + ), + }, + ], + ), + ); + }, + ); + } + + Widget _buildCommunitiesTree( + BuildContext context, + List communities, + ) { + return Expanded( + child: SpaceManagementSidebarCommunitiesList( + communities: communities, + itemBuilder: (context, index) { + return _buildCommunityTile(context, communities[index]); + }, + ), + ); + } + + Widget _buildCommunityTile(BuildContext context, CommunityModel community) { + final spaces = community.spaces + .where((space) => space.status == SpaceStatus.active) + .map((space) => _buildSpaceTile( + space: space, + community: community, + context: context, + )) + .toList(); + return CommunityTile( + title: community.name, + key: ValueKey(community.uuid), + isSelected: context + .watch() + .state + .selectedCommunity + ?.uuid == + community.uuid, + isExpanded: false, + onItemSelected: () { + context.read().add( + SelectCommunityEvent(community: community), + ); + }, + onExpansionChanged: (title, expanded) {}, + children: spaces, + ); + } + + Widget _buildSpaceTile({ + required SpaceModel space, + required CommunityModel community, + required BuildContext context, + }) { + final spaceIsExpanded = _isSpaceOrChildSelected(context, space); + final isSelected = + context.watch().state.selectedSpace?.uuid == + space.uuid; + return Padding( + padding: const EdgeInsetsDirectional.only(start: 16.0), + child: SpaceTile( + title: space.spaceName, + key: ValueKey(space.uuid), + isSelected: isSelected, + initiallyExpanded: spaceIsExpanded, + onExpansionChanged: (expanded) {}, + onItemSelected: () => context.read().add( + SelectSpaceEvent(space: space), + ), + children: space.children + .map( + (childSpace) => _buildSpaceTile( + space: childSpace, + community: community, + context: context, + ), + ) + .toList(), + ), + ); + } + + void _onAddCommunity(BuildContext context) => context + .read() + .state + .selectedCommunity + ?.uuid + .isNotEmpty ?? + true + ? _clearSelection(context) + : _showCreateCommunityDialog(context); + + void _clearSelection(BuildContext context) => + context.read().add( + const ClearCommunitiesTreeSelectionEvent(), + ); + + void _showCreateCommunityDialog(BuildContext context) => showDialog( + context: context, + builder: (context) => CreateCommunityDialog( + title: const Text('Community Name'), + onCreateCommunity: (name) {}, + ), + ); +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart new file mode 100644 index 00000000..ba281335 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class SpaceManagementSidebarAddCommunityButton extends StatelessWidget { + const SpaceManagementSidebarAddCommunityButton({ + required this.onTap, + super.key, + }); + + final void Function() onTap; + + @override + Widget build(BuildContext context) { + return SizedBox.square( + dimension: 30, + child: IconButton( + style: IconButton.styleFrom( + iconSize: 20, + backgroundColor: ColorsManager.circleImageBackground, + shape: const CircleBorder( + side: BorderSide( + color: ColorsManager.lightGrayBorderColor, + width: 3, + ), + ), + ), + onPressed: onTap, + icon: SvgPicture.asset(Assets.addIcon), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart new file mode 100644 index 00000000..e7cb1ef6 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SpaceManagementSidebarCommunitiesList extends StatefulWidget { + const SpaceManagementSidebarCommunitiesList({ + required this.communities, + required this.itemBuilder, + super.key, + }); + + final List communities; + final Widget Function(BuildContext context, int index) itemBuilder; + + @override + State createState() => + _SpaceManagementSidebarCommunitiesListState(); +} + +class _SpaceManagementSidebarCommunitiesListState + extends State { + late final ScrollController _scrollController; + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + } + + bool _onNotification(ScrollEndNotification notification) { + final hasReachedEnd = notification.metrics.extentAfter == 0; + if (hasReachedEnd) { + // Call data from API. + return true; + } + + return false; + } + + @override + void dispose() { + _scrollController + ..removeListener(() {}) + ..dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SizedBox( + width: context.screenWidth * 0.5, + child: Scrollbar( + scrollbarOrientation: ScrollbarOrientation.left, + thumbVisibility: true, + controller: _scrollController, + child: NotificationListener( + onNotification: _onNotification, + child: ListView.builder( + shrinkWrap: true, + padding: const EdgeInsetsDirectional.only(start: 16), + itemCount: widget.communities.length, + controller: _scrollController, + itemBuilder: widget.itemBuilder, + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart new file mode 100644 index 00000000..cf40f95c --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class SpaceManagementSidebarHeader extends StatelessWidget { + const SpaceManagementSidebarHeader({ + required this.onAddCommunity, + super.key, + }); + + final void Function() onAddCommunity; + + @override + Widget build(BuildContext context) { + return Container( + decoration: subSectionContainerDecoration, + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Communities', + style: context.textTheme.titleMedium?.copyWith( + color: ColorsManager.blackColor, + ), + ), + SpaceManagementSidebarAddCommunityButton( + onTap: onAddCommunity, + ), + ], + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart new file mode 100644 index 00000000..d05199f0 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/common/widgets/custom_expansion_tile.dart'; + +class SpaceTile extends StatefulWidget { + final String title; + final bool isSelected; + final bool initiallyExpanded; + final ValueChanged onExpansionChanged; + final List? children; + final void Function() onItemSelected; + + const SpaceTile({ + super.key, + required this.title, + required this.initiallyExpanded, + required this.onExpansionChanged, + required this.onItemSelected, + required this.isSelected, + this.children, + }); + + @override + State createState() => _SpaceTileState(); +} + +class _SpaceTileState extends State { + late bool _isExpanded; + + @override + void initState() { + super.initState(); + _isExpanded = widget.initiallyExpanded; + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 8.0), + child: CustomExpansionTile( + isSelected: widget.isSelected, + title: widget.title, + initiallyExpanded: _isExpanded, + onItemSelected: widget.onItemSelected, + onExpansionChanged: (bool expanded) { + setState(() { + _isExpanded = expanded; + }); + widget.onExpansionChanged(expanded); + }, + children: widget.children ?? [], + ), + ); + } +} diff --git a/lib/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart index d72f22ac..d81a3b04 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart @@ -4,11 +4,10 @@ import 'package:syncrow_web/common/widgets/custom_expansion_tile.dart'; class SpaceTile extends StatefulWidget { final String title; final bool isSelected; - final bool initiallyExpanded; final ValueChanged onExpansionChanged; final List? children; - final Function() onItemSelected; + final void Function() onItemSelected; const SpaceTile({ super.key, diff --git a/lib/utils/app_routes.dart b/lib/utils/app_routes.dart index 263bdbd6..7663a3f3 100644 --- a/lib/utils/app_routes.dart +++ b/lib/utils/app_routes.dart @@ -5,7 +5,7 @@ import 'package:syncrow_web/pages/auth/view/login_page.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/view/device_managment_page.dart'; import 'package:syncrow_web/pages/home/view/home_page.dart'; import 'package:syncrow_web/pages/roles_and_permission/view/roles_and_permission_page.dart'; -import 'package:syncrow_web/pages/spaces_management/all_spaces/view/spaces_management_page.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/views/space_management_page.dart'; import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart'; import 'package:syncrow_web/utils/constants/routes_const.dart'; From 51c088d9984f0860d36177cd1645d74d5ac87aa8 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 11:11:25 +0300 Subject: [PATCH 03/11] made communities paginatable. --- .../views/space_management_page.dart | 39 +++----- .../services/remote_communities_service.dart | 25 +++-- .../models/communities_pagination_model.dart | 69 +++++++++++++ .../domain/params/load_communities_param.dart | 33 ++++++- .../domain/services/communities_service.dart | 4 +- .../presentation/bloc/communities_bloc.dart | 99 ++++++++++++++++++- .../presentation/bloc/communities_event.dart | 16 +++ .../presentation/bloc/communities_state.dart | 38 ++++++- 8 files changed, 272 insertions(+), 51 deletions(-) create mode 100644 lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart diff --git a/lib/pages/space_management_v2/main_module/views/space_management_page.dart b/lib/pages/space_management_v2/main_module/views/space_management_page.dart index 4c3c7452..93e2684f 100644 --- a/lib/pages/space_management_v2/main_module/views/space_management_page.dart +++ b/lib/pages/space_management_v2/main_module/views/space_management_page.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_body.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; @@ -46,37 +46,20 @@ class SpaceManagementPage extends StatelessWidget { class _FakeCommunitiesService extends CommunitiesService { @override - Future> getCommunity(LoadCommunitiesParam param) async { - return Future.delayed( - const Duration(seconds: 1), - () => [ - const CommunityModel( + Future getCommunity(LoadCommunitiesParam param) { + return Future.value(const CommunitiesPaginationModel( + communities: [ + CommunityModel( uuid: '1', name: 'Community 1', - spaces: [ - SpaceModel( - uuid: '3', - spaceName: 'Space 1', - icon: 'assets/icons/space.png', - children: [ - SpaceModel( - uuid: '4', - spaceName: 'Space 2', - icon: 'assets/icons/space.png', - children: [], - status: SpaceStatus.active, - ), - ], - status: SpaceStatus.active, - ), - ], - ), - const CommunityModel( - uuid: '2', - name: 'Community 1', spaces: [], ), ], - ); + page: 1, + size: 10, + hasNext: false, + totalItems: 2, + totalPages: 1, + )); } } diff --git a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart index 83a212ca..e4202398 100644 --- a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart @@ -1,6 +1,6 @@ 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/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; @@ -15,28 +15,25 @@ class RemoteCommunitiesService implements CommunitiesService { static const _defaultErrorMessage = 'Failed to load communities'; @override - Future> getCommunity(LoadCommunitiesParam param) async { + Future getCommunity(LoadCommunitiesParam param) async { final projectUuid = await ProjectManager.getProjectUUID(); if (projectUuid == null) throw APIException('Project UUID is not set'); try { - final allCommunities = []; - await _httpService.get( + final response = await _httpService.get( path: await _makeUrl(), + queryParameters: { + 'page': param.page, + 'size': param.size, + 'includeSpaces': param.includeSpaces, + if (param.search.isNotEmpty) 'search': param.search, + }, expectedResponseModel: (json) { - final response = json as Map; - final jsonData = response['data'] as List? ?? []; - return jsonData - .map( - (jsonItem) => CommunityModel.fromJson( - jsonItem as Map, - ), - ) - .toList(); + return CommunitiesPaginationModel.fromJson(json as Map); }, ); - return allCommunities; + return response; } on DioException catch (e) { final message = e.response?.data as Map?; final error = message?['error'] as Map?; diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart new file mode 100644 index 00000000..f13ef8ba --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart @@ -0,0 +1,69 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; + +class CommunitiesPaginationModel extends Equatable { + const CommunitiesPaginationModel({ + required this.communities, + required this.page, + required this.size, + required this.hasNext, + required this.totalItems, + required this.totalPages, + }); + + final List communities; + final int page; + final int size; + final bool hasNext; + final int totalItems; + final int totalPages; + + const CommunitiesPaginationModel.empty() + : communities = const [], + page = 1, + size = 25, + hasNext = false, + totalItems = 0, + totalPages = 0; + + factory CommunitiesPaginationModel.fromJson(Map json) { + return CommunitiesPaginationModel( + communities: (json['data'] as List? ?? []) + .map((e) => CommunityModel.fromJson(e as Map)) + .toList(), + page: json['page'] as int? ?? 1, + size: json['size'] as int? ?? 25, + hasNext: json['hasNext'] as bool? ?? false, + totalItems: json['totalItems'] as int? ?? 0, + totalPages: json['totalPages'] as int? ?? 0, + ); + } + + CommunitiesPaginationModel copyWith({ + List? communities, + int? page, + int? size, + bool? hasNext, + int? totalItems, + int? totalPages, + }) { + return CommunitiesPaginationModel( + communities: communities ?? this.communities, + page: page ?? this.page, + size: size ?? this.size, + hasNext: hasNext ?? this.hasNext, + totalItems: totalItems ?? this.totalItems, + totalPages: totalPages ?? this.totalPages, + ); + } + + @override + List get props => [ + communities, + page, + size, + hasNext, + totalItems, + totalPages, + ]; +} diff --git a/lib/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart b/lib/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart index 9bdc215c..774c4c31 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart @@ -1,3 +1,32 @@ -class LoadCommunitiesParam { - const LoadCommunitiesParam(); +import 'package:equatable/equatable.dart'; + +class LoadCommunitiesParam extends Equatable { + const LoadCommunitiesParam({ + this.page = 1, + this.size = 25, + this.search = '', + this.includeSpaces = true, + }); + + final int page; + final int size; + final String search; + final bool includeSpaces; + + LoadCommunitiesParam copyWith({ + int? page, + int? size, + String? search, + bool? includeSpaces, + }) { + return LoadCommunitiesParam( + page: page ?? this.page, + size: size ?? this.size, + search: search ?? this.search, + includeSpaces: includeSpaces ?? this.includeSpaces, + ); + } + + @override + List get props => [page, size, search, includeSpaces]; } diff --git a/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart b/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart index bccad2ad..564dc4da 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart @@ -1,6 +1,6 @@ -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; abstract class CommunitiesService { - Future> getCommunity(LoadCommunitiesParam param); + Future getCommunity(LoadCommunitiesParam param); } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart index 0d85b22f..47dd43f8 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart @@ -14,6 +14,8 @@ class CommunitiesBloc extends Bloc { }) : _communitiesService = communitiesService, super(const CommunitiesState()) { on(_onLoadCommunities); + on(_onLoadMoreCommunities); + on(_onSearchCommunities); } final CommunitiesService _communitiesService; @@ -23,24 +25,113 @@ class CommunitiesBloc extends Bloc { Emitter emit, ) async { try { - emit(const CommunitiesState(status: CommunitiesStatus.loading)); - final communities = await _communitiesService.getCommunity(event.param); + emit(state.copyWith(status: CommunitiesStatus.loading)); + + final paginationResponse = await _communitiesService.getCommunity(event.param); + emit( CommunitiesState( status: CommunitiesStatus.success, - communities: communities, + communities: paginationResponse.communities, + hasNext: paginationResponse.hasNext, + currentPage: paginationResponse.page, + searchQuery: event.param.search, ), ); } on APIException catch (e) { emit( - CommunitiesState( + state.copyWith( status: CommunitiesStatus.failure, errorMessage: e.message, ), ); } catch (e) { + emit( + state.copyWith( + status: CommunitiesStatus.failure, + errorMessage: e.toString(), + ), + ); + } + } + + Future _onLoadMoreCommunities( + LoadMoreCommunities event, + Emitter emit, + ) async { + if (!state.hasNext || state.isLoadingMore) return; + + try { + emit(state.copyWith(isLoadingMore: true)); + + final param = LoadCommunitiesParam( + page: state.currentPage + 1, + search: state.searchQuery, + ); + + final paginationResponse = await _communitiesService.getCommunity(param); + + final updatedCommunities = List.from(state.communities) + ..addAll(paginationResponse.communities); + + emit( + state.copyWith( + communities: updatedCommunities, + hasNext: paginationResponse.hasNext, + currentPage: paginationResponse.page, + isLoadingMore: false, + ), + ); + } on APIException catch (e) { + emit( + state.copyWith( + isLoadingMore: false, + errorMessage: e.message, + ), + ); + } catch (e) { + emit( + state.copyWith( + isLoadingMore: false, + errorMessage: e.toString(), + ), + ); + } + } + + Future _onSearchCommunities( + SearchCommunities event, + Emitter emit, + ) async { + try { + emit(state.copyWith(status: CommunitiesStatus.loading)); + + final param = LoadCommunitiesParam( + page: 1, + search: event.searchQuery, + ); + + final paginationResponse = await _communitiesService.getCommunity(param); + emit( CommunitiesState( + status: CommunitiesStatus.success, + communities: paginationResponse.communities, + hasNext: paginationResponse.hasNext, + currentPage: paginationResponse.page, + searchQuery: event.searchQuery, + ), + ); + } on APIException catch (e) { + emit( + state.copyWith( + status: CommunitiesStatus.failure, + errorMessage: e.message, + ), + ); + } catch (e) { + emit( + state.copyWith( status: CommunitiesStatus.failure, errorMessage: e.toString(), ), diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart index ef375c5a..aa6eda17 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart @@ -15,3 +15,19 @@ class LoadCommunities extends CommunitiesEvent { @override List get props => [param]; } + +class LoadMoreCommunities extends CommunitiesEvent { + const LoadMoreCommunities(); + + @override + List get props => []; +} + +class SearchCommunities extends CommunitiesEvent { + const SearchCommunities(this.searchQuery); + + final String searchQuery; + + @override + List get props => [searchQuery]; +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_state.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_state.dart index 94740f0b..c0e57ffd 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_state.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_state.dart @@ -7,12 +7,48 @@ final class CommunitiesState extends Equatable { this.status = CommunitiesStatus.initial, this.communities = const [], this.errorMessage, + this.isLoadingMore = false, + this.hasNext = false, + this.currentPage = 1, + this.searchQuery = '', }); final CommunitiesStatus status; final List communities; final String? errorMessage; + final bool isLoadingMore; + final bool hasNext; + final int currentPage; + final String searchQuery; + + CommunitiesState copyWith({ + CommunitiesStatus? status, + List? communities, + String? errorMessage, + bool? isLoadingMore, + bool? hasNext, + int? currentPage, + String? searchQuery, + }) { + return CommunitiesState( + status: status ?? this.status, + communities: communities ?? this.communities, + errorMessage: errorMessage ?? this.errorMessage, + isLoadingMore: isLoadingMore ?? this.isLoadingMore, + hasNext: hasNext ?? this.hasNext, + currentPage: currentPage ?? this.currentPage, + searchQuery: searchQuery ?? this.searchQuery, + ); + } @override - List get props => [status, communities, errorMessage]; + List get props => [ + status, + communities, + errorMessage, + isLoadingMore, + hasNext, + currentPage, + searchQuery, + ]; } From 65ed94eb089d5c135ce2ec456471a23d7d069efa Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 12:01:32 +0300 Subject: [PATCH 04/11] debounce and refactored `CommunitiesBloc`. --- .../debounced_communities_service.dart | 60 ++++++++++++ .../presentation/bloc/communities_bloc.dart | 82 ++++------------ .../presentation/bloc/communities_event.dart | 9 -- .../space_management_communities_tree.dart | 97 ++++++++++++++++--- ...e_management_sidebar_communities_list.dart | 44 ++++++++- 5 files changed, 204 insertions(+), 88 deletions(-) create mode 100644 lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart diff --git a/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart new file mode 100644 index 00000000..f8bd56d1 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart @@ -0,0 +1,60 @@ +import 'dart:async'; + +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; + +class DebouncedCommunitiesService implements CommunitiesService { + DebouncedCommunitiesService({ + required CommunitiesService communitiesService, + this.debounceDuration = const Duration(milliseconds: 400), + }) : _communitiesService = communitiesService; + + final CommunitiesService _communitiesService; + final Duration debounceDuration; + + Timer? _debounceTimer; + String _lastSearchQuery = ''; + + @override + Future getCommunity( + LoadCommunitiesParam param, + ) async { + if (param.search.isNotEmpty) { + return _getDebouncedCommunity(param); + } + + return _communitiesService.getCommunity(param); + } + + Future _getDebouncedCommunity( + LoadCommunitiesParam param, + ) async { + final completer = Completer(); + + _debounceTimer?.cancel(); + + _lastSearchQuery = param.search; + + _debounceTimer = Timer(debounceDuration, () async { + try { + if (_lastSearchQuery == param.search) { + final result = await _communitiesService.getCommunity(param); + if (!completer.isCompleted) { + completer.complete(result); + } + } else { + if (!completer.isCompleted) { + completer.complete(const CommunitiesPaginationModel.empty()); + } + } + } catch (error) { + if (!completer.isCompleted) { + completer.completeError(error); + } + } + }); + + return completer.future; + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart index 47dd43f8..53eb9d3f 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart @@ -15,7 +15,6 @@ class CommunitiesBloc extends Bloc { super(const CommunitiesState()) { on(_onLoadCommunities); on(_onLoadMoreCommunities); - on(_onSearchCommunities); } final CommunitiesService _communitiesService; @@ -39,19 +38,9 @@ class CommunitiesBloc extends Bloc { ), ); } on APIException catch (e) { - emit( - state.copyWith( - status: CommunitiesStatus.failure, - errorMessage: e.message, - ), - ); + _onApiException(e, emit); } catch (e) { - emit( - state.copyWith( - status: CommunitiesStatus.failure, - errorMessage: e.toString(), - ), - ); + _onError(e, emit); } } @@ -83,59 +72,30 @@ class CommunitiesBloc extends Bloc { ), ); } on APIException catch (e) { - emit( - state.copyWith( - isLoadingMore: false, - errorMessage: e.message, - ), - ); + _onApiException(e, emit); } catch (e) { - emit( - state.copyWith( - isLoadingMore: false, - errorMessage: e.toString(), - ), - ); + _onError(e, emit); } } - Future _onSearchCommunities( - SearchCommunities event, + void _onApiException( + APIException e, Emitter emit, - ) async { - try { - emit(state.copyWith(status: CommunitiesStatus.loading)); + ) { + emit( + state.copyWith( + isLoadingMore: false, + errorMessage: e.message, + ), + ); + } - final param = LoadCommunitiesParam( - page: 1, - search: event.searchQuery, - ); - - final paginationResponse = await _communitiesService.getCommunity(param); - - emit( - CommunitiesState( - status: CommunitiesStatus.success, - communities: paginationResponse.communities, - hasNext: paginationResponse.hasNext, - currentPage: paginationResponse.page, - searchQuery: event.searchQuery, - ), - ); - } on APIException catch (e) { - emit( - state.copyWith( - status: CommunitiesStatus.failure, - errorMessage: e.message, - ), - ); - } catch (e) { - emit( - state.copyWith( - status: CommunitiesStatus.failure, - errorMessage: e.toString(), - ), - ); - } + void _onError(Object e, Emitter emit) { + emit( + state.copyWith( + isLoadingMore: false, + errorMessage: e.toString(), + ), + ); } } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart index aa6eda17..ae4d86bf 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart @@ -22,12 +22,3 @@ class LoadMoreCommunities extends CommunitiesEvent { @override List get props => []; } - -class SearchCommunities extends CommunitiesEvent { - const SearchCommunities(this.searchQuery); - - final String searchQuery; - - @override - List get props => [searchQuery]; -} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart index 3248fa7d..51322b52 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart @@ -1,20 +1,36 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/common/widgets/search_bar.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.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/widgets/community_tile.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.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_tile.dart'; import 'package:syncrow_web/utils/style.dart'; -class SpaceManagementCommunitiesTree extends StatelessWidget { +class SpaceManagementCommunitiesTree extends StatefulWidget { const SpaceManagementCommunitiesTree({super.key}); + @override + State createState() => + _SpaceManagementCommunitiesTreeState(); +} + +class _SpaceManagementCommunitiesTreeState + extends State { + @override + void initState() { + context.read().add( + const LoadCommunities(LoadCommunitiesParam()), + ); + super.initState(); + } + bool _isSpaceOrChildSelected(BuildContext context, SpaceModel space) { final selectedSpace = context.read().state.selectedSpace; @@ -25,6 +41,18 @@ class SpaceManagementCommunitiesTree extends StatelessWidget { return isSpaceSelected || anySubSpaceIsSelected; } + void _onSearchChanged(String searchQuery) { + context.read().add( + LoadCommunities(LoadCommunitiesParam( + search: searchQuery.trim(), + )), + ); + } + + void _onLoadMore() { + context.read().add(const LoadMoreCommunities()); + } + static const _width = 300.0; @override @@ -39,18 +67,18 @@ class SpaceManagementCommunitiesTree extends StatelessWidget { SpaceManagementSidebarHeader( onAddCommunity: () => _onAddCommunity(context), ), - CustomSearchBar(onSearchChanged: (value) {}), + CustomSearchBar( + onSearchChanged: _onSearchChanged, + ), const SizedBox(height: 16), switch (state.status) { CommunitiesStatus.initial => const Center(child: CircularProgressIndicator()), - CommunitiesStatus.loading => - const Center(child: CircularProgressIndicator()), - CommunitiesStatus.success => - _buildCommunitiesTree(context, state.communities), - CommunitiesStatus.failure => Center( - child: Text(state.errorMessage ?? 'Something went wrong'), - ), + CommunitiesStatus.loading => state.communities.isEmpty + ? const Center(child: CircularProgressIndicator()) + : _buildCommunitiesTree(context, state), + CommunitiesStatus.success => _buildCommunitiesTree(context, state), + CommunitiesStatus.failure => _buildErrorState(context, state), }, ], ), @@ -59,15 +87,58 @@ class SpaceManagementCommunitiesTree extends StatelessWidget { ); } + Widget _buildErrorState(BuildContext context, CommunitiesState state) { + return Expanded( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + state.errorMessage ?? 'Something went wrong', + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + context.read().add( + LoadCommunities(LoadCommunitiesParam( + search: state.searchQuery, + )), + ); + }, + child: const Text('Retry'), + ), + ], + ), + ), + ); + } + Widget _buildCommunitiesTree( BuildContext context, - List communities, + CommunitiesState state, ) { + if (state.communities.isEmpty && state.status == CommunitiesStatus.success) { + return Expanded( + child: Center( + child: Text( + state.searchQuery.isEmpty + ? 'No communities found' + : 'No communities found for "${state.searchQuery}"', + textAlign: TextAlign.center, + ), + ), + ); + } + return Expanded( child: SpaceManagementSidebarCommunitiesList( - communities: communities, + communities: state.communities, + onLoadMore: state.hasNext ? _onLoadMore : null, + isLoadingMore: state.isLoadingMore, + hasNext: state.hasNext, itemBuilder: (context, index) { - return _buildCommunityTile(context, communities[index]); + return _buildCommunityTile(context, state.communities[index]); }, ), ); diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart index e7cb1ef6..68119dcd 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart @@ -6,11 +6,17 @@ class SpaceManagementSidebarCommunitiesList extends StatefulWidget { const SpaceManagementSidebarCommunitiesList({ required this.communities, required this.itemBuilder, + this.onLoadMore, + this.isLoadingMore = false, + this.hasNext = false, super.key, }); final List communities; final Widget Function(BuildContext context, int index) itemBuilder; + final VoidCallback? onLoadMore; + final bool isLoadingMore; + final bool hasNext; @override State createState() => @@ -25,12 +31,26 @@ class _SpaceManagementSidebarCommunitiesListState void initState() { super.initState(); _scrollController = ScrollController(); + _scrollController.addListener(_onScroll); + } + + void _onScroll() { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 100) { + // Trigger pagination when user is close to the bottom + if (widget.hasNext && !widget.isLoadingMore && widget.onLoadMore != null) { + widget.onLoadMore!(); + } + } } bool _onNotification(ScrollEndNotification notification) { final hasReachedEnd = notification.metrics.extentAfter == 0; - if (hasReachedEnd) { - // Call data from API. + if (hasReachedEnd && + widget.hasNext && + !widget.isLoadingMore && + widget.onLoadMore != null) { + widget.onLoadMore!(); return true; } @@ -40,13 +60,16 @@ class _SpaceManagementSidebarCommunitiesListState @override void dispose() { _scrollController - ..removeListener(() {}) + ..removeListener(_onScroll) ..dispose(); super.dispose(); } @override Widget build(BuildContext context) { + // Calculate item count including loading indicator + final itemCount = widget.communities.length + (widget.isLoadingMore ? 1 : 0); + return SingleChildScrollView( scrollDirection: Axis.horizontal, child: SizedBox( @@ -60,9 +83,20 @@ class _SpaceManagementSidebarCommunitiesListState child: ListView.builder( shrinkWrap: true, padding: const EdgeInsetsDirectional.only(start: 16), - itemCount: widget.communities.length, + itemCount: itemCount, controller: _scrollController, - itemBuilder: widget.itemBuilder, + itemBuilder: (context, index) { + if (index == widget.communities.length) { + return const Padding( + padding: EdgeInsets.all(16.0), + child: Center( + child: CircularProgressIndicator(), + ), + ); + } + + return widget.itemBuilder(context, index); + }, ), ), ), From 8494f0a8f1a484148bdae6afec928c53cf738720 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 12:21:46 +0300 Subject: [PATCH 05/11] matched community and space models with API. --- .../views/space_management_page.dart | 30 +++-------- .../debounced_communities_service.dart | 51 +++++++------------ .../services/remote_communities_service.dart | 18 ++++--- .../models/communities_pagination_model.dart | 4 +- .../domain/models/community_model.dart | 12 +++++ .../domain/models/space_model.dart | 27 ++++------ .../space_management_communities_tree.dart | 1 - lib/utils/constants/api_const.dart | 1 + 8 files changed, 62 insertions(+), 82 deletions(-) diff --git a/lib/pages/space_management_v2/main_module/views/space_management_page.dart b/lib/pages/space_management_v2/main_module/views/space_management_page.dart index 93e2684f..957be65a 100644 --- a/lib/pages/space_management_v2/main_module/views/space_management_page.dart +++ b/lib/pages/space_management_v2/main_module/views/space_management_page.dart @@ -2,12 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_body.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.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/services/api/http_service.dart'; import 'package:syncrow_web/utils/theme/responsive_text_theme.dart'; import 'package:syncrow_web/web_layout/web_scaffold.dart'; @@ -20,7 +20,9 @@ class SpaceManagementPage extends StatelessWidget { providers: [ BlocProvider( create: (context) => CommunitiesBloc( - communitiesService: _FakeCommunitiesService(), + communitiesService: DebouncedCommunitiesService( + RemoteCommunitiesService(HTTPService()), + ), )..add(const LoadCommunities(LoadCommunitiesParam())), ), BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()), @@ -43,23 +45,3 @@ class SpaceManagementPage extends StatelessWidget { ); } } - -class _FakeCommunitiesService extends CommunitiesService { - @override - Future getCommunity(LoadCommunitiesParam param) { - return Future.value(const CommunitiesPaginationModel( - communities: [ - CommunityModel( - uuid: '1', - name: 'Community 1', - spaces: [], - ), - ], - page: 1, - size: 10, - hasNext: false, - totalItems: 2, - totalPages: 1, - )); - } -} diff --git a/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart index f8bd56d1..ca1923f9 100644 --- a/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart @@ -4,57 +4,44 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; -class DebouncedCommunitiesService implements CommunitiesService { - DebouncedCommunitiesService({ - required CommunitiesService communitiesService, - this.debounceDuration = const Duration(milliseconds: 400), - }) : _communitiesService = communitiesService; +final class DebouncedCommunitiesService implements CommunitiesService { + DebouncedCommunitiesService( + this._decoratee, { + this.debounceDuration = const Duration(milliseconds: 500), + }); - final CommunitiesService _communitiesService; + final CommunitiesService _decoratee; final Duration debounceDuration; Timer? _debounceTimer; - String _lastSearchQuery = ''; + Completer? _completer; @override Future getCommunity( LoadCommunitiesParam param, ) async { - if (param.search.isNotEmpty) { - return _getDebouncedCommunity(param); - } - - return _communitiesService.getCommunity(param); - } - - Future _getDebouncedCommunity( - LoadCommunitiesParam param, - ) async { - final completer = Completer(); - _debounceTimer?.cancel(); - _lastSearchQuery = param.search; + if (_completer != null && !_completer!.isCompleted) { + _completer!.completeError(Exception('Request cancelled by newer request')); + } + + _completer = Completer(); + final currentCompleter = _completer!; _debounceTimer = Timer(debounceDuration, () async { try { - if (_lastSearchQuery == param.search) { - final result = await _communitiesService.getCommunity(param); - if (!completer.isCompleted) { - completer.complete(result); - } - } else { - if (!completer.isCompleted) { - completer.complete(const CommunitiesPaginationModel.empty()); - } + final result = await _decoratee.getCommunity(param); + if (!currentCompleter.isCompleted) { + currentCompleter.complete(result); } } catch (error) { - if (!completer.isCompleted) { - completer.completeError(error); + if (!currentCompleter.isCompleted) { + currentCompleter.completeError(error); } } }); - return completer.future; + return currentCompleter.future; } } diff --git a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart index e4202398..925a1cd0 100644 --- a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart @@ -15,10 +15,9 @@ class RemoteCommunitiesService implements CommunitiesService { static const _defaultErrorMessage = 'Failed to load communities'; @override - Future getCommunity(LoadCommunitiesParam param) async { - final projectUuid = await ProjectManager.getProjectUUID(); - if (projectUuid == null) throw APIException('Project UUID is not set'); - + Future getCommunity( + LoadCommunitiesParam param, + ) async { try { final response = await _httpService.get( path: await _makeUrl(), @@ -26,10 +25,12 @@ class RemoteCommunitiesService implements CommunitiesService { 'page': param.page, 'size': param.size, 'includeSpaces': param.includeSpaces, - if (param.search.isNotEmpty) 'search': param.search, + if (param.search.isNotEmpty && param.search != 'null') + 'search': param.search, }, expectedResponseModel: (json) { - return CommunitiesPaginationModel.fromJson(json as Map); + final data = json as Map; + return CommunitiesPaginationModel.fromJson(data); }, ); @@ -48,6 +49,9 @@ class RemoteCommunitiesService implements CommunitiesService { Future _makeUrl() async { final projectUuid = await ProjectManager.getProjectUUID(); if (projectUuid == null) throw APIException('Project UUID is required'); - return ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectUuid); + return ApiEndpoints.getCommunityListv2.replaceAll( + '{projectId}', + projectUuid, + ); } } diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart index f13ef8ba..a86783be 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart @@ -34,8 +34,8 @@ class CommunitiesPaginationModel extends Equatable { page: json['page'] as int? ?? 1, size: json['size'] as int? ?? 25, hasNext: json['hasNext'] as bool? ?? false, - totalItems: json['totalItems'] as int? ?? 0, - totalPages: json['totalPages'] as int? ?? 0, + totalItems: json['totalItem'] as int? ?? 0, + totalPages: json['totalPage'] as int? ?? 0, ); } diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart index c6efad9e..ea0839f9 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart @@ -4,11 +4,19 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain class CommunityModel extends Equatable { final String uuid; final String name; + final DateTime createdAt; + final DateTime updatedAt; + final String description; + final String externalId; final List spaces; const CommunityModel({ required this.uuid, required this.name, + required this.createdAt, + required this.updatedAt, + required this.description, + required this.externalId, required this.spaces, }); @@ -16,6 +24,10 @@ class CommunityModel extends Equatable { return CommunityModel( uuid: json['uuid'] as String, name: json['name'] as String, + createdAt: DateTime.parse(json['createdAt'] as String), + updatedAt: DateTime.parse(json['updatedAt'] as String), + description: json['description'] as String, + externalId: json['externalId'] as String, spaces: (json['spaces'] as List) .map((e) => SpaceModel.fromJson(e as Map)) .toList(), diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart index 519e8ee7..d6007815 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart @@ -1,43 +1,38 @@ import 'package:equatable/equatable.dart'; -enum SpaceStatus { - active, - deleted, - parentDeleted; - - static SpaceStatus getValueFromString(String value) => switch (value) { - 'active' => active, - 'deleted' => deleted, - 'parentDeleted' => parentDeleted, - _ => active, - }; -} - class SpaceModel extends Equatable { final String uuid; + final DateTime createdAt; + final DateTime updatedAt; final String spaceName; final String icon; final List children; - final SpaceStatus status; + final SpaceModel? parent; const SpaceModel({ required this.uuid, + required this.createdAt, + required this.updatedAt, required this.spaceName, required this.icon, required this.children, - required this.status, + required this.parent, }); factory SpaceModel.fromJson(Map json) { return SpaceModel( uuid: json['uuid'] as String, + createdAt: DateTime.parse(json['createdAt'] as String), + updatedAt: DateTime.parse(json['updatedAt'] as String), spaceName: json['spaceName'] as String, icon: json['icon'] as String, children: (json['children'] as List?) ?.map((e) => SpaceModel.fromJson(e as Map)) .toList() ?? [], - status: SpaceStatus.getValueFromString(json['status'] as String), + parent: json['parent'] != null + ? SpaceModel.fromJson(json['parent'] as Map) + : null, ); } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart index 51322b52..b9902bd6 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart @@ -146,7 +146,6 @@ class _SpaceManagementCommunitiesTreeState Widget _buildCommunityTile(BuildContext context, CommunityModel community) { final spaces = community.spaces - .where((space) => space.status == SpaceStatus.active) .map((space) => _buildSpaceTile( space: space, community: community, diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index d58d0f28..048f3000 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -46,6 +46,7 @@ abstract class ApiEndpoints { // Community Module static const String createCommunity = '/projects/{projectId}/communities'; static const String getCommunityList = '/projects/{projectId}/communities'; + static const String getCommunityListv2 = '/projects/{projectId}/communities/v2'; static const String getCommunityById = '/projects/{projectId}/communities/{communityId}'; static const String updateCommunity = From b79ab06d9523ff11058be8e12a1c1aed30cf2a9d Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 12:46:17 +0300 Subject: [PATCH 06/11] shows a loading indicator when loading. --- .../debounced_communities_service.dart | 6 +-- .../presentation/bloc/communities_bloc.dart | 12 +++++- .../space_management_communities_tree.dart | 41 +++++++++++++------ ...e_management_sidebar_communities_list.dart | 2 - 4 files changed, 39 insertions(+), 22 deletions(-) diff --git a/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart index ca1923f9..e512679b 100644 --- a/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart @@ -14,7 +14,7 @@ final class DebouncedCommunitiesService implements CommunitiesService { final Duration debounceDuration; Timer? _debounceTimer; - Completer? _completer; + late Completer? _completer; @override Future getCommunity( @@ -22,10 +22,6 @@ final class DebouncedCommunitiesService implements CommunitiesService { ) async { _debounceTimer?.cancel(); - if (_completer != null && !_completer!.isCompleted) { - _completer!.completeError(Exception('Request cancelled by newer request')); - } - _completer = Completer(); final currentCompleter = _completer!; diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart index 53eb9d3f..ef91baa2 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart @@ -24,9 +24,13 @@ class CommunitiesBloc extends Bloc { Emitter emit, ) async { try { - emit(state.copyWith(status: CommunitiesStatus.loading)); + emit( + state.copyWith(status: CommunitiesStatus.loading), + ); - final paginationResponse = await _communitiesService.getCommunity(event.param); + final paginationResponse = await _communitiesService.getCommunity( + event.param, + ); emit( CommunitiesState( @@ -35,6 +39,7 @@ class CommunitiesBloc extends Bloc { hasNext: paginationResponse.hasNext, currentPage: paginationResponse.page, searchQuery: event.param.search, + isLoadingMore: false, ), ); } on APIException catch (e) { @@ -65,6 +70,7 @@ class CommunitiesBloc extends Bloc { emit( state.copyWith( + status: CommunitiesStatus.success, communities: updatedCommunities, hasNext: paginationResponse.hasNext, currentPage: paginationResponse.page, @@ -84,6 +90,7 @@ class CommunitiesBloc extends Bloc { ) { emit( state.copyWith( + status: CommunitiesStatus.failure, isLoadingMore: false, errorMessage: e.message, ), @@ -93,6 +100,7 @@ class CommunitiesBloc extends Bloc { void _onError(Object e, Emitter emit) { emit( state.copyWith( + status: CommunitiesStatus.failure, isLoadingMore: false, errorMessage: e.toString(), ), diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart index b9902bd6..4501cf7e 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart @@ -42,11 +42,9 @@ class _SpaceManagementCommunitiesTreeState } void _onSearchChanged(String searchQuery) { - context.read().add( - LoadCommunities(LoadCommunitiesParam( - search: searchQuery.trim(), - )), - ); + context + .read() + .add(LoadCommunities(LoadCommunitiesParam(search: searchQuery.trim()))); } void _onLoadMore() { @@ -80,6 +78,13 @@ class _SpaceManagementCommunitiesTreeState CommunitiesStatus.success => _buildCommunitiesTree(context, state), CommunitiesStatus.failure => _buildErrorState(context, state), }, + Visibility( + visible: state.isLoadingMore, + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Center(child: CircularProgressIndicator()), + ), + ), ], ), ); @@ -132,14 +137,24 @@ class _SpaceManagementCommunitiesTreeState } return Expanded( - child: SpaceManagementSidebarCommunitiesList( - communities: state.communities, - onLoadMore: state.hasNext ? _onLoadMore : null, - isLoadingMore: state.isLoadingMore, - hasNext: state.hasNext, - itemBuilder: (context, index) { - return _buildCommunityTile(context, state.communities[index]); - }, + child: Stack( + children: [ + SpaceManagementSidebarCommunitiesList( + communities: state.communities, + onLoadMore: state.hasNext ? _onLoadMore : null, + isLoadingMore: state.isLoadingMore, + hasNext: state.hasNext, + itemBuilder: (context, index) { + return _buildCommunityTile(context, state.communities[index]); + }, + ), + if (state.status == CommunitiesStatus.loading && + state.communities.isNotEmpty) + ColoredBox( + color: Colors.white.withValues(alpha: 0.7), + child: const Center(child: CircularProgressIndicator()), + ), + ], ), ); } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart index 68119dcd..40766be5 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart @@ -37,7 +37,6 @@ class _SpaceManagementSidebarCommunitiesListState void _onScroll() { if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 100) { - // Trigger pagination when user is close to the bottom if (widget.hasNext && !widget.isLoadingMore && widget.onLoadMore != null) { widget.onLoadMore!(); } @@ -67,7 +66,6 @@ class _SpaceManagementSidebarCommunitiesListState @override Widget build(BuildContext context) { - // Calculate item count including loading indicator final itemCount = widget.communities.length + (widget.isLoadingMore ? 1 : 0); return SingleChildScrollView( From f02788eaa5835255e7707f266e53cea6dd860c11 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 14:58:38 +0300 Subject: [PATCH 07/11] implemented create community feature. --- .../domain/models/community_model.dart | 4 +- .../presentation/bloc/communities_bloc.dart | 8 + .../presentation/bloc/communities_event.dart | 9 + .../widgets/create_community_dialog.dart | 181 ------------------ .../space_management_communities_tree.dart | 33 ++-- .../remote_create_community_service.dart | 50 +++-- .../domain/param/create_community_param.dart | 8 +- .../presentation/create_community_dialog.dart | 61 ++++++ .../create_community_dialog_widget.dart | 144 ++++++++++++++ .../create_community_name_text_field.dart | 48 +++++ 10 files changed, 338 insertions(+), 208 deletions(-) delete mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart create mode 100644 lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart create mode 100644 lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart create mode 100644 lib/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart index ea0839f9..344dbff5 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart @@ -27,8 +27,8 @@ class CommunityModel extends Equatable { createdAt: DateTime.parse(json['createdAt'] as String), updatedAt: DateTime.parse(json['updatedAt'] as String), description: json['description'] as String, - externalId: json['externalId'] as String, - spaces: (json['spaces'] as List) + externalId: json['externalId']?.toString() ?? '', + spaces: (json['spaces'] as List? ?? []) .map((e) => SpaceModel.fromJson(e as Map)) .toList(), ); diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart index ef91baa2..0f754b06 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart @@ -15,6 +15,7 @@ class CommunitiesBloc extends Bloc { super(const CommunitiesState()) { on(_onLoadCommunities); on(_onLoadMoreCommunities); + on(_onInsertCommunity); } final CommunitiesService _communitiesService; @@ -106,4 +107,11 @@ class CommunitiesBloc extends Bloc { ), ); } + + void _onInsertCommunity( + InsertCommunity event, + Emitter emit, + ) { + emit(state.copyWith(communities: [event.community, ...state.communities])); + } } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart index ae4d86bf..cd14fa3d 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart @@ -22,3 +22,12 @@ class LoadMoreCommunities extends CommunitiesEvent { @override List get props => []; } + +final class InsertCommunity extends CommunitiesEvent { + const InsertCommunity(this.community); + + final CommunityModel community; + + @override + List get props => [community]; +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart deleted file mode 100644 index fd8a0a68..00000000 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.dart +++ /dev/null @@ -1,181 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; -import 'package:syncrow_web/pages/common/buttons/default_button.dart'; -import 'package:syncrow_web/pages/spaces_management/create_community/bloc/community_dialog_bloc.dart'; -import 'package:syncrow_web/pages/spaces_management/create_community/bloc/community_dialog_event.dart'; -import 'package:syncrow_web/pages/spaces_management/create_community/bloc/community_dialog_state.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; - -class CreateCommunityDialog extends StatefulWidget { - final void Function(String name) onCreateCommunity; - final String? initialName; - final Widget title; - - const CreateCommunityDialog({ - super.key, - required this.onCreateCommunity, - required this.title, - this.initialName, - }); - - @override - State createState() => _CreateCommunityDialogState(); -} - -class _CreateCommunityDialogState extends State { - late final TextEditingController _nameController; - - @override - void initState() { - _nameController = TextEditingController(text: widget.initialName ?? ''); - super.initState(); - } - - @override - void dispose() { - _nameController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => CommunityDialogBloc([]), - child: Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - backgroundColor: ColorsManager.transparentColor, - child: Stack( - children: [ - Container( - width: MediaQuery.of(context).size.width * 0.3, - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: ColorsManager.whiteColors, - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: ColorsManager.blackColor.withOpacity(0.25), - blurRadius: 20, - spreadRadius: 5, - offset: const Offset(0, 5), - ), - ], - ), - child: SingleChildScrollView( - child: BlocBuilder( - builder: (context, state) { - var isNameValid = true; - var isNameEmpty = false; - - if (state is CommunityNameValidationState) { - isNameValid = state.isNameValid; - isNameEmpty = state.isNameEmpty; - } - - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - DefaultTextStyle( - style: Theme.of(context).textTheme.headlineMedium!, - child: widget.title, - ), - const SizedBox(height: 18), - TextField( - controller: _nameController, - onChanged: (value) { - context - .read() - .add(ValidateCommunityNameEvent(value)); - }, - style: Theme.of(context).textTheme.bodyMedium, - decoration: InputDecoration( - hintText: 'Please enter the community name', - filled: true, - fillColor: ColorsManager.boxColor, - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: isNameValid && !isNameEmpty - ? ColorsManager.boxColor - : ColorsManager.red, - width: 1, - ), - borderRadius: BorderRadius.circular(10), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide( - color: ColorsManager.boxColor, - width: 1.5, - ), - ), - ), - ), - if (!isNameValid) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - '*Name already exists.', - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith(color: ColorsManager.red), - ), - ), - if (isNameEmpty) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - '*Name should not be empty.', - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith(color: ColorsManager.red), - ), - ), - const SizedBox(height: 24), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: CancelButton( - label: 'Cancel', - onPressed: () => Navigator.of(context).pop(), - ), - ), - const SizedBox(width: 16), - Expanded( - child: DefaultButton( - onPressed: () { - if (isNameValid && !isNameEmpty) { - widget.onCreateCommunity( - _nameController.text.trim(), - ); - Navigator.of(context).pop(); - } - }, - backgroundColor: isNameValid && !isNameEmpty - ? ColorsManager.secondaryColor - : ColorsManager.lightGrayColor, - borderRadius: 10, - foregroundColor: ColorsManager.whiteColors, - child: const Text('OK'), - ), - ), - ], - ), - ], - ); - }, - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart index 4501cf7e..efafdd85 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart @@ -7,10 +7,10 @@ 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/communities/presentation/widgets/community_tile.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/create_community_dialog.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_tile.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart'; import 'package:syncrow_web/utils/style.dart'; class SpaceManagementCommunitiesTree extends StatefulWidget { @@ -220,15 +220,17 @@ class _SpaceManagementCommunitiesTreeState ); } - void _onAddCommunity(BuildContext context) => context - .read() - .state - .selectedCommunity - ?.uuid - .isNotEmpty ?? - true - ? _clearSelection(context) - : _showCreateCommunityDialog(context); + void _onAddCommunity(BuildContext context) { + context + .read() + .state + .selectedCommunity + ?.uuid + .isNotEmpty ?? + false + ? _clearSelection(context) + : _showCreateCommunityDialog(context); + } void _clearSelection(BuildContext context) => context.read().add( @@ -237,9 +239,16 @@ class _SpaceManagementCommunitiesTreeState void _showCreateCommunityDialog(BuildContext context) => showDialog( context: context, - builder: (context) => CreateCommunityDialog( + builder: (_) => CreateCommunityDialog( title: const Text('Community Name'), - onCreateCommunity: (name) {}, + onCreateCommunity: (community) { + context.read().add( + InsertCommunity(community), + ); + context.read().add( + SelectCommunityEvent(community: community), + ); + }, ), ); } diff --git a/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart b/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart index be83124b..bd91f6ce 100644 --- a/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart +++ b/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart @@ -1,4 +1,5 @@ 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/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/services/create_community_service.dart'; @@ -16,24 +17,51 @@ class RemoteCreateCommunityService implements CreateCommunityService { Future createCommunity(CreateCommunityParam param) async { try { final response = await _httpService.post( - path: 'endpoint', - expectedResponseModel: (data) => CommunityModel.fromJson( - data as Map, - ), + path: await _makeUrl(), + body: { + 'name': param.name, + 'description': param.description, + }, + expectedResponseModel: (data) { + final json = data as Map; + if (json['success'] == true) { + return CommunityModel.fromJson( + json['data'] as Map, + ); + } + return null; + }, ); + + if (response == null) { + throw APIException( + _getErrorMessageFromBody(response as Map?), + ); + } return response; } on DioException catch (e) { final message = e.response?.data as Map?; - final error = message?['error'] as Map?; - final errorMessage = error?['error'] as String? ?? ''; - final formattedErrorMessage = [ - _defaultErrorMessage, - errorMessage, - ].join(': '); - throw APIException(formattedErrorMessage); + throw APIException(_getErrorMessageFromBody(message)); } catch (e) { final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': '); throw APIException(formattedErrorMessage); } } + + String _getErrorMessageFromBody(Map? body) { + if (body == null) { + return _defaultErrorMessage; + } + final error = body['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + return errorMessage; + } + + Future _makeUrl() async { + final projectUuid = await ProjectManager.getProjectUUID(); + if (projectUuid == null) { + throw APIException('Project UUID is not set'); + } + return '/projects/$projectUuid/communities'; + } } diff --git a/lib/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart b/lib/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart index 3d7c203b..68a9fa11 100644 --- a/lib/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart +++ b/lib/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart @@ -1,9 +1,13 @@ import 'package:equatable/equatable.dart'; class CreateCommunityParam extends Equatable { - const CreateCommunityParam({required this.name}); - + const CreateCommunityParam({ + required this.name, + this.description = '', + }); + final String name; + final String description; @override List get props => [name]; diff --git a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart new file mode 100644 index 00000000..8c1d474d --- /dev/null +++ b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/bloc/create_community_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; + +class CreateCommunityDialog extends StatelessWidget { + final void Function(CommunityModel community) onCreateCommunity; + final String? initialName; + final Widget title; + + const CreateCommunityDialog({ + super.key, + required this.onCreateCommunity, + required this.title, + this.initialName, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => CreateCommunityBloc(RemoteCreateCommunityService(HTTPService())), + child: BlocListener( + listener: (context, state) { + switch (state) { + case CreateCommunityLoading(): + showDialog( + context: context, + builder: (context) => const Center( + child: CircularProgressIndicator(), + ), + ); + break; + case CreateCommunitySuccess(:final community): + Navigator.of(context).pop(); + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Community created successfully')), + ); + onCreateCommunity.call(community); + break; + case CreateCommunityFailure(:final message): + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message)), + ); + break; + default: + break; + } + }, + child: CreateCommunityDialogWidget( + title: title, + initialName: initialName, + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart new file mode 100644 index 00000000..49d43ae6 --- /dev/null +++ b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/bloc/create_community_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CreateCommunityDialogWidget extends StatefulWidget { + final String? initialName; + final Widget title; + + const CreateCommunityDialogWidget({ + super.key, + required this.title, + this.initialName, + }); + + @override + State createState() => + _CreateCommunityDialogWidgetState(); +} + +class _CreateCommunityDialogWidgetState extends State { + late final TextEditingController _nameController; + + @override + void initState() { + _nameController = TextEditingController(text: widget.initialName ?? ''); + super.initState(); + } + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + final _formKey = GlobalKey(); + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + backgroundColor: ColorsManager.transparentColor, + child: Container( + width: MediaQuery.of(context).size.width * 0.3, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: ColorsManager.blackColor.withValues(alpha: 0.25), + blurRadius: 20, + spreadRadius: 5, + offset: const Offset(0, 5), + ), + ], + ), + child: Form( + key: _formKey, + child: SingleChildScrollView( + child: BlocBuilder( + builder: (context, state) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DefaultTextStyle( + style: Theme.of(context).textTheme.headlineMedium!, + child: widget.title, + ), + const SizedBox(height: 18), + CreateCommunityNameTextField( + nameController: _nameController, + ), + if (state case CreateCommunityFailure(:final message)) + Padding( + padding: const EdgeInsets.only(top: 18), + child: SelectableText( + '* $message', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.error, + ), + ), + ), + const SizedBox(height: 24), + _buildActionButtons(context), + ], + ); + }, + ), + ), + ), + ), + ); + } + + Row _buildActionButtons(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: CancelButton( + label: 'Cancel', + onPressed: () => Navigator.of(context).pop(), + ), + ), + const SizedBox(width: 16), + _buildCreateCommunityButton(context), + ], + ); + } + + Widget _buildCreateCommunityButton(BuildContext context) { + return Expanded( + child: DefaultButton( + onPressed: () { + if (_formKey.currentState?.validate() ?? false) { + _onSubmit(context); + } + }, + borderRadius: 10, + foregroundColor: ColorsManager.whiteColors, + child: const Text('OK'), + ), + ); + } + + void _onSubmit(BuildContext context) { + if (_formKey.currentState?.validate() ?? false) { + context.read().add( + CreateCommunity( + CreateCommunityParam( + name: _nameController.text.trim(), + ), + ), + ); + } + } +} diff --git a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart new file mode 100644 index 00000000..d42474d5 --- /dev/null +++ b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class CreateCommunityNameTextField extends StatelessWidget { + const CreateCommunityNameTextField({ + required this.nameController, + super.key, + }); + + final TextEditingController nameController; + + @override + Widget build(BuildContext context) { + return TextFormField( + controller: nameController, + validator: _validator, + style: context.textTheme.bodyMedium, + decoration: InputDecoration( + hintText: 'Please enter the community name', + filled: true, + fillColor: ColorsManager.boxColor, + enabledBorder: _buildBorder(ColorsManager.boxColor), + focusedBorder: _buildBorder(), + focusedErrorBorder: _buildBorder(Theme.of(context).colorScheme.error), + errorBorder: _buildBorder(Theme.of(context).colorScheme.error), + ), + ); + } + + String? _validator(String? value) { + if (value == null || value.isEmpty) { + return '*Name should not be empty.'; + } + + return null; + } + + InputBorder _buildBorder([Color? color]) { + return OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: color ?? ColorsManager.vividBlue.withValues(alpha: 0.5), + width: 1, + ), + ); + } +} From 09446844b0e689b829ad586ac6f09cd1eea46e1c Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 15:11:38 +0300 Subject: [PATCH 08/11] reverted initializing the new space management page in the router, to avoid any confusion with the QA team. --- lib/utils/app_routes.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/app_routes.dart b/lib/utils/app_routes.dart index 7663a3f3..263bdbd6 100644 --- a/lib/utils/app_routes.dart +++ b/lib/utils/app_routes.dart @@ -5,7 +5,7 @@ import 'package:syncrow_web/pages/auth/view/login_page.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/view/device_managment_page.dart'; import 'package:syncrow_web/pages/home/view/home_page.dart'; import 'package:syncrow_web/pages/roles_and_permission/view/roles_and_permission_page.dart'; -import 'package:syncrow_web/pages/space_management_v2/main_module/views/space_management_page.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/view/spaces_management_page.dart'; import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart'; import 'package:syncrow_web/utils/constants/routes_const.dart'; From 28ac911f3f6f939b3e20dc140f56dd012086f1e0 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 15:30:47 +0300 Subject: [PATCH 09/11] Accomodated for null values in `SpaceModel`. --- .../communities/domain/models/space_model.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart index d6007815..36943adb 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart @@ -2,8 +2,8 @@ import 'package:equatable/equatable.dart'; class SpaceModel extends Equatable { final String uuid; - final DateTime createdAt; - final DateTime updatedAt; + final DateTime? createdAt; + final DateTime? updatedAt; final String spaceName; final String icon; final List children; @@ -21,11 +21,11 @@ class SpaceModel extends Equatable { factory SpaceModel.fromJson(Map json) { return SpaceModel( - uuid: json['uuid'] as String, - createdAt: DateTime.parse(json['createdAt'] as String), - updatedAt: DateTime.parse(json['updatedAt'] as String), - spaceName: json['spaceName'] as String, - icon: json['icon'] as String, + uuid: json['uuid'] as String? ?? '', + createdAt: DateTime.tryParse(json['createdAt'] as String? ?? ''), + updatedAt: DateTime.tryParse(json['updatedAt'] as String? ?? ''), + spaceName: json['spaceName'] as String? ?? '', + icon: json['icon'] as String? ?? 'assets/icons/location_icon.svg', children: (json['children'] as List?) ?.map((e) => SpaceModel.fromJson(e as Map)) .toList() ?? From 41d4fbb555585a3cda5da9221d2a7faa06543638 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 22 Jun 2025 16:00:20 +0300 Subject: [PATCH 10/11] Extracted pagination data into a generic DTO. --- .../shared/models/paginated_data_model.dart | 45 ++++++++++++ .../debounced_communities_service.dart | 1 - .../services/remote_communities_service.dart | 4 +- .../models/communities_pagination_model.dart | 69 ------------------- .../domain/services/communities_service.dart | 5 +- .../presentation/bloc/communities_bloc.dart | 4 +- 6 files changed, 53 insertions(+), 75 deletions(-) create mode 100644 lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart delete mode 100644 lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart diff --git a/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart b/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart new file mode 100644 index 00000000..e37cd0a1 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart @@ -0,0 +1,45 @@ +import 'package:equatable/equatable.dart'; + +class PaginatedDataModel extends Equatable { + const PaginatedDataModel({ + required this.data, + required this.page, + required this.size, + required this.hasNext, + required this.totalItems, + required this.totalPages, + }); + + final List data; + final int page; + final int size; + final bool hasNext; + final int totalItems; + final int totalPages; + + factory PaginatedDataModel.fromJson( + Map json, + T Function(Map) fromJsonT, + ) { + return PaginatedDataModel( + data: (json['data'] as List? ?? []) + .map((e) => fromJsonT(e as Map)) + .toList(), + page: json['page'] as int? ?? 1, + size: json['size'] as int? ?? 25, + hasNext: json['hasNext'] as bool? ?? false, + totalItems: json['totalItem'] as int? ?? 0, + totalPages: json['totalPage'] as int? ?? 0, + ); + } + + @override + List get props => [ + data, + page, + size, + hasNext, + totalItems, + totalPages, + ]; +} diff --git a/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart index e512679b..a97e8524 100644 --- a/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; diff --git a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart index 925a1cd0..b58961a6 100644 --- a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart @@ -1,6 +1,6 @@ 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/communities_pagination_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; @@ -30,7 +30,7 @@ class RemoteCommunitiesService implements CommunitiesService { }, expectedResponseModel: (json) { final data = json as Map; - return CommunitiesPaginationModel.fromJson(data); + return CommunitiesPaginationModel.fromJson(data, CommunityModel.fromJson); }, ); diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart deleted file mode 100644 index a86783be..00000000 --- a/lib/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; - -class CommunitiesPaginationModel extends Equatable { - const CommunitiesPaginationModel({ - required this.communities, - required this.page, - required this.size, - required this.hasNext, - required this.totalItems, - required this.totalPages, - }); - - final List communities; - final int page; - final int size; - final bool hasNext; - final int totalItems; - final int totalPages; - - const CommunitiesPaginationModel.empty() - : communities = const [], - page = 1, - size = 25, - hasNext = false, - totalItems = 0, - totalPages = 0; - - factory CommunitiesPaginationModel.fromJson(Map json) { - return CommunitiesPaginationModel( - communities: (json['data'] as List? ?? []) - .map((e) => CommunityModel.fromJson(e as Map)) - .toList(), - page: json['page'] as int? ?? 1, - size: json['size'] as int? ?? 25, - hasNext: json['hasNext'] as bool? ?? false, - totalItems: json['totalItem'] as int? ?? 0, - totalPages: json['totalPage'] as int? ?? 0, - ); - } - - CommunitiesPaginationModel copyWith({ - List? communities, - int? page, - int? size, - bool? hasNext, - int? totalItems, - int? totalPages, - }) { - return CommunitiesPaginationModel( - communities: communities ?? this.communities, - page: page ?? this.page, - size: size ?? this.size, - hasNext: hasNext ?? this.hasNext, - totalItems: totalItems ?? this.totalItems, - totalPages: totalPages ?? this.totalPages, - ); - } - - @override - List get props => [ - communities, - page, - size, - hasNext, - totalItems, - totalPages, - ]; -} diff --git a/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart b/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart index 564dc4da..baa84590 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart @@ -1,6 +1,9 @@ -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/communities_pagination_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; +typedef CommunitiesPaginationModel = PaginatedDataModel; + abstract class CommunitiesService { Future getCommunity(LoadCommunitiesParam param); } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart index 0f754b06..9094a632 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart @@ -36,7 +36,7 @@ class CommunitiesBloc extends Bloc { emit( CommunitiesState( status: CommunitiesStatus.success, - communities: paginationResponse.communities, + communities: paginationResponse.data, hasNext: paginationResponse.hasNext, currentPage: paginationResponse.page, searchQuery: event.param.search, @@ -67,7 +67,7 @@ class CommunitiesBloc extends Bloc { final paginationResponse = await _communitiesService.getCommunity(param); final updatedCommunities = List.from(state.communities) - ..addAll(paginationResponse.communities); + ..addAll(paginationResponse.data); emit( state.copyWith( From 27349a6cc0956878a0b590f8ffd66e2d7245bc7e Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 09:24:53 +0300 Subject: [PATCH 11/11] Implemented PR notes by extracting widgets into their own classes. --- lib/common/widgets/app_loading_indicator.dart | 10 + .../shared/models/paginated_data_model.dart | 6 +- .../services/remote_communities_service.dart | 8 +- .../domain/models/community_model.dart | 5 + .../communities_tree_failure_widget.dart | 38 +++ ...communities_tree_search_result_widget.dart | 22 ++ .../space_management_communities_tree.dart | 240 ++++-------------- ...ement_communities_tree_community_tile.dart | 45 ++++ ...anagement_communities_tree_space_tile.dart | 56 ++++ .../space_management_sidebar_header.dart | 46 +++- .../create_community_dialog_widget.dart | 2 +- 11 files changed, 271 insertions(+), 207 deletions(-) create mode 100644 lib/common/widgets/app_loading_indicator.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/empty_communities_tree_search_result_widget.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_community_tile.dart create mode 100644 lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart diff --git a/lib/common/widgets/app_loading_indicator.dart b/lib/common/widgets/app_loading_indicator.dart new file mode 100644 index 00000000..bc811c56 --- /dev/null +++ b/lib/common/widgets/app_loading_indicator.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class AppLoadingIndicator extends StatelessWidget { + const AppLoadingIndicator({super.key}); + + @override + Widget build(BuildContext context) { + return const Center(child: CircularProgressIndicator()); + } +} diff --git a/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart b/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart index e37cd0a1..ac35975d 100644 --- a/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart +++ b/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart @@ -19,12 +19,10 @@ class PaginatedDataModel extends Equatable { factory PaginatedDataModel.fromJson( Map json, - T Function(Map) fromJsonT, + List Function(List) fromJsonList, ) { return PaginatedDataModel( - data: (json['data'] as List? ?? []) - .map((e) => fromJsonT(e as Map)) - .toList(), + data: fromJsonList(json['data'] as List), page: json['page'] as int? ?? 1, size: json['size'] as int? ?? 25, hasNext: json['hasNext'] as bool? ?? false, diff --git a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart index b58961a6..cc842de8 100644 --- a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart @@ -28,10 +28,10 @@ class RemoteCommunitiesService implements CommunitiesService { if (param.search.isNotEmpty && param.search != 'null') 'search': param.search, }, - expectedResponseModel: (json) { - final data = json as Map; - return CommunitiesPaginationModel.fromJson(data, CommunityModel.fromJson); - }, + expectedResponseModel: (json) => CommunitiesPaginationModel.fromJson( + json as Map, + CommunityModel.fromJsonList, + ), ); return response; diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart index 344dbff5..37f131b3 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart @@ -33,6 +33,11 @@ class CommunityModel extends Equatable { .toList(), ); } + static List fromJsonList(List json) { + return json + .map((e) => CommunityModel.fromJson(e as Map)) + .toList(); + } @override List get props => [uuid, name, spaces]; diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart new file mode 100644 index 00000000..cfd32f52 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; + +class CommunitiesTreeFailureWidget extends StatelessWidget { + const CommunitiesTreeFailureWidget({super.key, this.errorMessage}); + + final String? errorMessage; + + @override + Widget build(BuildContext context) { + return Expanded( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + errorMessage ?? 'Something went wrong', + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => context.read().add( + LoadCommunities( + LoadCommunitiesParam( + search: context.read().state.searchQuery, + ), + ), + ), + child: const Text('Retry'), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/empty_communities_tree_search_result_widget.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/empty_communities_tree_search_result_widget.dart new file mode 100644 index 00000000..bfc9e30e --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/empty_communities_tree_search_result_widget.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class EmptyCommunitiesTreeSearchResultWidget extends StatelessWidget { + const EmptyCommunitiesTreeSearchResultWidget({ + required this.searchQuery, + super.key, + }); + + final String searchQuery; + + @override + Widget build(BuildContext context) { + return Center( + child: Text( + searchQuery.isEmpty + ? 'No communities found' + : 'No communities found for "$searchQuery"', + textAlign: TextAlign.center, + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart index efafdd85..1adf9911 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart @@ -1,16 +1,14 @@ 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/common/widgets/search_bar.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.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/widgets/community_tile.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/empty_communities_tree_search_result_widget.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_header.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart'; -import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart'; import 'package:syncrow_web/utils/style.dart'; class SpaceManagementCommunitiesTree extends StatefulWidget { @@ -31,16 +29,6 @@ class _SpaceManagementCommunitiesTreeState super.initState(); } - bool _isSpaceOrChildSelected(BuildContext context, SpaceModel space) { - final selectedSpace = - context.read().state.selectedSpace; - final isSpaceSelected = selectedSpace?.uuid == space.uuid; - final anySubSpaceIsSelected = space.children.any( - (child) => _isSpaceOrChildSelected(context, child), - ); - return isSpaceSelected || anySubSpaceIsSelected; - } - void _onSearchChanged(String searchQuery) { context .read() @@ -51,67 +39,32 @@ class _SpaceManagementCommunitiesTreeState context.read().add(const LoadMoreCommunities()); } - static const _width = 300.0; - @override Widget build(BuildContext context) { return BlocBuilder( - builder: (context, state) { - return Container( - width: _width, - decoration: subSectionContainerDecoration, - child: Column( - children: [ - SpaceManagementSidebarHeader( - onAddCommunity: () => _onAddCommunity(context), - ), - CustomSearchBar( - onSearchChanged: _onSearchChanged, - ), - const SizedBox(height: 16), - switch (state.status) { - CommunitiesStatus.initial => - const Center(child: CircularProgressIndicator()), - CommunitiesStatus.loading => state.communities.isEmpty - ? const Center(child: CircularProgressIndicator()) - : _buildCommunitiesTree(context, state), - CommunitiesStatus.success => _buildCommunitiesTree(context, state), - CommunitiesStatus.failure => _buildErrorState(context, state), - }, - Visibility( - visible: state.isLoadingMore, - child: const Padding( - padding: EdgeInsets.all(8.0), - child: Center(child: CircularProgressIndicator()), - ), - ), - ], - ), - ); - }, - ); - } - - Widget _buildErrorState(BuildContext context, CommunitiesState state) { - return Expanded( - child: Center( + builder: (context, state) => Container( + width: 320, + decoration: subSectionContainerDecoration, child: Column( - mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - state.errorMessage ?? 'Something went wrong', - textAlign: TextAlign.center, + const SpaceManagementSidebarHeader(), + CustomSearchBar( + onSearchChanged: _onSearchChanged, ), const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - context.read().add( - LoadCommunities(LoadCommunitiesParam( - search: state.searchQuery, - )), - ); - }, - child: const Text('Retry'), + switch (state.status) { + CommunitiesStatus.initial => const AppLoadingIndicator(), + CommunitiesStatus.loading => state.communities.isEmpty + ? const AppLoadingIndicator() + : _buildCommunitiesTree(context, state), + CommunitiesStatus.success => _buildCommunitiesTree(context, state), + CommunitiesStatus.failure => CommunitiesTreeFailureWidget( + errorMessage: state.errorMessage, + ), + }, + Visibility( + visible: state.isLoadingMore, + child: const AppLoadingIndicator(), ), ], ), @@ -123,132 +76,37 @@ class _SpaceManagementCommunitiesTreeState BuildContext context, CommunitiesState state, ) { - if (state.communities.isEmpty && state.status == CommunitiesStatus.success) { - return Expanded( - child: Center( - child: Text( - state.searchQuery.isEmpty - ? 'No communities found' - : 'No communities found for "${state.searchQuery}"', - textAlign: TextAlign.center, - ), - ), - ); - } + final communitiesIsEmpty = state.communities.isEmpty; + final statusIsSuccess = state.status == CommunitiesStatus.success; return Expanded( - child: Stack( - children: [ - SpaceManagementSidebarCommunitiesList( - communities: state.communities, - onLoadMore: state.hasNext ? _onLoadMore : null, - isLoadingMore: state.isLoadingMore, - hasNext: state.hasNext, - itemBuilder: (context, index) { - return _buildCommunityTile(context, state.communities[index]); - }, - ), - if (state.status == CommunitiesStatus.loading && - state.communities.isNotEmpty) - ColoredBox( - color: Colors.white.withValues(alpha: 0.7), - child: const Center(child: CircularProgressIndicator()), + child: Visibility( + visible: statusIsSuccess && communitiesIsEmpty, + replacement: Stack( + children: [ + SpaceManagementSidebarCommunitiesList( + communities: state.communities, + onLoadMore: state.hasNext ? _onLoadMore : null, + isLoadingMore: state.isLoadingMore, + hasNext: state.hasNext, + itemBuilder: (context, index) { + return SpaceManagementCommunitiesTreeCommunityTile( + community: state.communities[index], + ); + }, ), - ], - ), - ); - } - - Widget _buildCommunityTile(BuildContext context, CommunityModel community) { - final spaces = community.spaces - .map((space) => _buildSpaceTile( - space: space, - community: community, - context: context, - )) - .toList(); - return CommunityTile( - title: community.name, - key: ValueKey(community.uuid), - isSelected: context - .watch() - .state - .selectedCommunity - ?.uuid == - community.uuid, - isExpanded: false, - onItemSelected: () { - context.read().add( - SelectCommunityEvent(community: community), - ); - }, - onExpansionChanged: (title, expanded) {}, - children: spaces, - ); - } - - Widget _buildSpaceTile({ - required SpaceModel space, - required CommunityModel community, - required BuildContext context, - }) { - final spaceIsExpanded = _isSpaceOrChildSelected(context, space); - final isSelected = - context.watch().state.selectedSpace?.uuid == - space.uuid; - return Padding( - padding: const EdgeInsetsDirectional.only(start: 16.0), - child: SpaceTile( - title: space.spaceName, - key: ValueKey(space.uuid), - isSelected: isSelected, - initiallyExpanded: spaceIsExpanded, - onExpansionChanged: (expanded) {}, - onItemSelected: () => context.read().add( - SelectSpaceEvent(space: space), - ), - children: space.children - .map( - (childSpace) => _buildSpaceTile( - space: childSpace, - community: community, - context: context, + if (state.status == CommunitiesStatus.loading && + state.communities.isNotEmpty) + ColoredBox( + color: Colors.white.withValues(alpha: 0.7), + child: const AppLoadingIndicator(), ), - ) - .toList(), + ], + ), + child: EmptyCommunitiesTreeSearchResultWidget( + searchQuery: state.searchQuery, + ), ), ); } - - void _onAddCommunity(BuildContext context) { - context - .read() - .state - .selectedCommunity - ?.uuid - .isNotEmpty ?? - false - ? _clearSelection(context) - : _showCreateCommunityDialog(context); - } - - void _clearSelection(BuildContext context) => - context.read().add( - const ClearCommunitiesTreeSelectionEvent(), - ); - - void _showCreateCommunityDialog(BuildContext context) => showDialog( - context: context, - builder: (_) => CreateCommunityDialog( - title: const Text('Community Name'), - onCreateCommunity: (community) { - context.read().add( - InsertCommunity(community), - ); - context.read().add( - SelectCommunityEvent(community: community), - ); - }, - ), - ); } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_community_tile.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_community_tile.dart new file mode 100644 index 00000000..736a499f --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_community_tile.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart'; + +class SpaceManagementCommunitiesTreeCommunityTile extends StatelessWidget { + const SpaceManagementCommunitiesTreeCommunityTile({ + required this.community, + super.key, + }); + + final CommunityModel community; + + @override + Widget build(BuildContext context) { + final spaces = community.spaces + .map( + (space) => SpaceManagementCommunitiesTreeSpaceTile( + space: space, + community: community, + ), + ) + .toList(); + return CommunityTile( + title: community.name, + key: ValueKey(community.uuid), + isSelected: context + .watch() + .state + .selectedCommunity + ?.uuid == + community.uuid, + isExpanded: false, + onItemSelected: () { + context.read().add( + SelectCommunityEvent(community: community), + ); + }, + onExpansionChanged: (title, expanded) {}, + children: spaces, + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart new file mode 100644 index 00000000..dcd44ac8 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart'; + +class SpaceManagementCommunitiesTreeSpaceTile extends StatelessWidget { + const SpaceManagementCommunitiesTreeSpaceTile({ + required this.space, + required this.community, + super.key, + }); + + final SpaceModel space; + final CommunityModel community; + + @override + Widget build(BuildContext context) { + final spaceIsExpanded = _isSpaceOrChildSelected(context, space); + final isSelected = + context.watch().state.selectedSpace?.uuid == + space.uuid; + return Padding( + padding: const EdgeInsetsDirectional.only(start: 16.0), + child: SpaceTile( + title: space.spaceName, + key: ValueKey(space.uuid), + isSelected: isSelected, + initiallyExpanded: spaceIsExpanded, + onExpansionChanged: (expanded) {}, + onItemSelected: () => context.read().add( + SelectSpaceEvent(space: space), + ), + children: space.children + .map( + (childSpace) => SpaceManagementCommunitiesTreeSpaceTile( + space: childSpace, + community: community, + ), + ) + .toList(), + ), + ); + } + + bool _isSpaceOrChildSelected(BuildContext context, SpaceModel space) { + final selectedSpace = + context.read().state.selectedSpace; + final isSpaceSelected = selectedSpace?.uuid == space.uuid; + final anySubSpaceIsSelected = space.children.any( + (child) => _isSpaceOrChildSelected(context, child), + ); + return isSpaceSelected || anySubSpaceIsSelected; + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart index cf40f95c..25c094db 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart @@ -1,16 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_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/widgets/space_management_sidebar_add_community_button.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/style.dart'; class SpaceManagementSidebarHeader extends StatelessWidget { - const SpaceManagementSidebarHeader({ - required this.onAddCommunity, - super.key, - }); - - final void Function() onAddCommunity; + const SpaceManagementSidebarHeader({super.key}); @override Widget build(BuildContext context) { @@ -27,10 +26,43 @@ class SpaceManagementSidebarHeader extends StatelessWidget { ), ), SpaceManagementSidebarAddCommunityButton( - onTap: onAddCommunity, + onTap: () => _onAddCommunity(context), ), ], ), ); } + + void _onAddCommunity(BuildContext context) { + final bloc = context.read(); + final selectedCommunity = bloc.state.selectedCommunity; + final isSelected = selectedCommunity?.uuid.isNotEmpty ?? false; + + if (isSelected) { + _clearSelection(context); + } else { + _showCreateCommunityDialog(context); + } + } + + void _clearSelection(BuildContext context) { + context.read().add( + const ClearCommunitiesTreeSelectionEvent(), + ); + } + + void _showCreateCommunityDialog(BuildContext context) => showDialog( + context: context, + builder: (_) => CreateCommunityDialog( + title: const Text('Community Name'), + onCreateCommunity: (community) { + context.read().add( + InsertCommunity(community), + ); + context.read().add( + SelectCommunityEvent(community: community), + ); + }, + ), + ); } diff --git a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart index 49d43ae6..ab9f7b9a 100644 --- a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart +++ b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart @@ -99,7 +99,7 @@ class _CreateCommunityDialogWidgetState extends State