From 27349a6cc0956878a0b590f8ffd66e2d7245bc7e Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 23 Jun 2025 09:24:53 +0300 Subject: [PATCH] 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