Implemented PR notes by extracting widgets into their own classes.

This commit is contained in:
Faris Armoush
2025-06-23 09:24:53 +03:00
parent 41d4fbb555
commit 27349a6cc0
11 changed files with 271 additions and 207 deletions

View File

@ -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());
}
}

View File

@ -19,12 +19,10 @@ class PaginatedDataModel<T> extends Equatable {
factory PaginatedDataModel.fromJson(
Map<String, dynamic> json,
T Function(Map<String, dynamic>) fromJsonT,
List<T> Function(List<dynamic>) fromJsonList,
) {
return PaginatedDataModel<T>(
data: (json['data'] as List<dynamic>? ?? [])
.map((e) => fromJsonT(e as Map<String, dynamic>))
.toList(),
data: fromJsonList(json['data'] as List<dynamic>),
page: json['page'] as int? ?? 1,
size: json['size'] as int? ?? 25,
hasNext: json['hasNext'] as bool? ?? false,

View File

@ -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<String, dynamic>;
return CommunitiesPaginationModel.fromJson(data, CommunityModel.fromJson);
},
expectedResponseModel: (json) => CommunitiesPaginationModel.fromJson(
json as Map<String, dynamic>,
CommunityModel.fromJsonList,
),
);
return response;

View File

@ -33,6 +33,11 @@ class CommunityModel extends Equatable {
.toList(),
);
}
static List<CommunityModel> fromJsonList(List<dynamic> json) {
return json
.map((e) => CommunityModel.fromJson(e as Map<String, dynamic>))
.toList();
}
@override
List<Object?> get props => [uuid, name, spaces];

View File

@ -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<CommunitiesBloc>().add(
LoadCommunities(
LoadCommunitiesParam(
search: context.read<CommunitiesBloc>().state.searchQuery,
),
),
),
child: const Text('Retry'),
),
],
),
),
);
}
}

View File

@ -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,
),
);
}
}

View File

@ -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<CommunitiesTreeSelectionBloc>().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<CommunitiesBloc>()
@ -51,67 +39,32 @@ class _SpaceManagementCommunitiesTreeState
context.read<CommunitiesBloc>().add(const LoadMoreCommunities());
}
static const _width = 300.0;
@override
Widget build(BuildContext context) {
return BlocBuilder<CommunitiesBloc, CommunitiesState>(
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<CommunitiesBloc>().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<CommunitiesTreeSelectionBloc>()
.state
.selectedCommunity
?.uuid ==
community.uuid,
isExpanded: false,
onItemSelected: () {
context.read<CommunitiesTreeSelectionBloc>().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<CommunitiesTreeSelectionBloc>().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<CommunitiesTreeSelectionBloc>().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<CommunitiesTreeSelectionBloc>()
.state
.selectedCommunity
?.uuid
.isNotEmpty ??
false
? _clearSelection(context)
: _showCreateCommunityDialog(context);
}
void _clearSelection(BuildContext context) =>
context.read<CommunitiesTreeSelectionBloc>().add(
const ClearCommunitiesTreeSelectionEvent(),
);
void _showCreateCommunityDialog(BuildContext context) => showDialog<void>(
context: context,
builder: (_) => CreateCommunityDialog(
title: const Text('Community Name'),
onCreateCommunity: (community) {
context.read<CommunitiesBloc>().add(
InsertCommunity(community),
);
context.read<CommunitiesTreeSelectionBloc>().add(
SelectCommunityEvent(community: community),
);
},
),
);
}

View File

@ -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<CommunitiesTreeSelectionBloc>()
.state
.selectedCommunity
?.uuid ==
community.uuid,
isExpanded: false,
onItemSelected: () {
context.read<CommunitiesTreeSelectionBloc>().add(
SelectCommunityEvent(community: community),
);
},
onExpansionChanged: (title, expanded) {},
children: spaces,
);
}
}

View File

@ -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<CommunitiesTreeSelectionBloc>().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<CommunitiesTreeSelectionBloc>().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<CommunitiesTreeSelectionBloc>().state.selectedSpace;
final isSpaceSelected = selectedSpace?.uuid == space.uuid;
final anySubSpaceIsSelected = space.children.any(
(child) => _isSpaceOrChildSelected(context, child),
);
return isSpaceSelected || anySubSpaceIsSelected;
}
}

View File

@ -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<CommunitiesTreeSelectionBloc>();
final selectedCommunity = bloc.state.selectedCommunity;
final isSelected = selectedCommunity?.uuid.isNotEmpty ?? false;
if (isSelected) {
_clearSelection(context);
} else {
_showCreateCommunityDialog(context);
}
}
void _clearSelection(BuildContext context) {
context.read<CommunitiesTreeSelectionBloc>().add(
const ClearCommunitiesTreeSelectionEvent(),
);
}
void _showCreateCommunityDialog(BuildContext context) => showDialog<void>(
context: context,
builder: (_) => CreateCommunityDialog(
title: const Text('Community Name'),
onCreateCommunity: (community) {
context.read<CommunitiesBloc>().add(
InsertCommunity(community),
);
context.read<CommunitiesTreeSelectionBloc>().add(
SelectCommunityEvent(community: community),
);
},
),
);
}

View File

@ -99,7 +99,7 @@ class _CreateCommunityDialogWidgetState extends State<CreateCommunityDialogWidge
);
}
Row _buildActionButtons(BuildContext context) {
Widget _buildActionButtons(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [