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( factory PaginatedDataModel.fromJson(
Map<String, dynamic> json, Map<String, dynamic> json,
T Function(Map<String, dynamic>) fromJsonT, List<T> Function(List<dynamic>) fromJsonList,
) { ) {
return PaginatedDataModel<T>( return PaginatedDataModel<T>(
data: (json['data'] as List<dynamic>? ?? []) data: fromJsonList(json['data'] as List<dynamic>),
.map((e) => fromJsonT(e as Map<String, dynamic>))
.toList(),
page: json['page'] as int? ?? 1, page: json['page'] as int? ?? 1,
size: json['size'] as int? ?? 25, size: json['size'] as int? ?? 25,
hasNext: json['hasNext'] as bool? ?? false, hasNext: json['hasNext'] as bool? ?? false,

View File

@ -28,10 +28,10 @@ class RemoteCommunitiesService implements CommunitiesService {
if (param.search.isNotEmpty && param.search != 'null') if (param.search.isNotEmpty && param.search != 'null')
'search': param.search, 'search': param.search,
}, },
expectedResponseModel: (json) { expectedResponseModel: (json) => CommunitiesPaginationModel.fromJson(
final data = json as Map<String, dynamic>; json as Map<String, dynamic>,
return CommunitiesPaginationModel.fromJson(data, CommunityModel.fromJson); CommunityModel.fromJsonList,
}, ),
); );
return response; return response;

View File

@ -33,6 +33,11 @@ class CommunityModel extends Equatable {
.toList(), .toList(),
); );
} }
static List<CommunityModel> fromJsonList(List<dynamic> json) {
return json
.map((e) => CommunityModel.fromJson(e as Map<String, dynamic>))
.toList();
}
@override @override
List<Object?> get props => [uuid, name, spaces]; 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/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/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/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/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/communities_tree_failure_widget.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/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_communities_list.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart';
import 'package:syncrow_web/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'; import 'package:syncrow_web/utils/style.dart';
class SpaceManagementCommunitiesTree extends StatefulWidget { class SpaceManagementCommunitiesTree extends StatefulWidget {
@ -31,16 +29,6 @@ class _SpaceManagementCommunitiesTreeState
super.initState(); 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) { void _onSearchChanged(String searchQuery) {
context context
.read<CommunitiesBloc>() .read<CommunitiesBloc>()
@ -51,67 +39,32 @@ class _SpaceManagementCommunitiesTreeState
context.read<CommunitiesBloc>().add(const LoadMoreCommunities()); context.read<CommunitiesBloc>().add(const LoadMoreCommunities());
} }
static const _width = 300.0;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<CommunitiesBloc, CommunitiesState>( return BlocBuilder<CommunitiesBloc, CommunitiesState>(
builder: (context, state) { builder: (context, state) => Container(
return Container( width: 320,
width: _width, decoration: subSectionContainerDecoration,
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(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( const SpaceManagementSidebarHeader(),
state.errorMessage ?? 'Something went wrong', CustomSearchBar(
textAlign: TextAlign.center, onSearchChanged: _onSearchChanged,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
ElevatedButton( switch (state.status) {
onPressed: () { CommunitiesStatus.initial => const AppLoadingIndicator(),
context.read<CommunitiesBloc>().add( CommunitiesStatus.loading => state.communities.isEmpty
LoadCommunities(LoadCommunitiesParam( ? const AppLoadingIndicator()
search: state.searchQuery, : _buildCommunitiesTree(context, state),
)), CommunitiesStatus.success => _buildCommunitiesTree(context, state),
); CommunitiesStatus.failure => CommunitiesTreeFailureWidget(
}, errorMessage: state.errorMessage,
child: const Text('Retry'), ),
},
Visibility(
visible: state.isLoadingMore,
child: const AppLoadingIndicator(),
), ),
], ],
), ),
@ -123,132 +76,37 @@ class _SpaceManagementCommunitiesTreeState
BuildContext context, BuildContext context,
CommunitiesState state, CommunitiesState state,
) { ) {
if (state.communities.isEmpty && state.status == CommunitiesStatus.success) { final communitiesIsEmpty = state.communities.isEmpty;
return Expanded( final statusIsSuccess = state.status == CommunitiesStatus.success;
child: Center(
child: Text(
state.searchQuery.isEmpty
? 'No communities found'
: 'No communities found for "${state.searchQuery}"',
textAlign: TextAlign.center,
),
),
);
}
return Expanded( return Expanded(
child: Stack( child: Visibility(
children: [ visible: statusIsSuccess && communitiesIsEmpty,
SpaceManagementSidebarCommunitiesList( replacement: Stack(
communities: state.communities, children: [
onLoadMore: state.hasNext ? _onLoadMore : null, SpaceManagementSidebarCommunitiesList(
isLoadingMore: state.isLoadingMore, communities: state.communities,
hasNext: state.hasNext, onLoadMore: state.hasNext ? _onLoadMore : null,
itemBuilder: (context, index) { isLoadingMore: state.isLoadingMore,
return _buildCommunityTile(context, state.communities[index]); hasNext: state.hasNext,
}, itemBuilder: (context, index) {
), return SpaceManagementCommunitiesTreeCommunityTile(
if (state.status == CommunitiesStatus.loading && community: state.communities[index],
state.communities.isNotEmpty) );
ColoredBox( },
color: Colors.white.withValues(alpha: 0.7),
child: const Center(child: CircularProgressIndicator()),
), ),
], if (state.status == CommunitiesStatus.loading &&
), state.communities.isNotEmpty)
); ColoredBox(
} color: Colors.white.withValues(alpha: 0.7),
child: const AppLoadingIndicator(),
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,
), ),
) ],
.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/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/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/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/style.dart'; import 'package:syncrow_web/utils/style.dart';
class SpaceManagementSidebarHeader extends StatelessWidget { class SpaceManagementSidebarHeader extends StatelessWidget {
const SpaceManagementSidebarHeader({ const SpaceManagementSidebarHeader({super.key});
required this.onAddCommunity,
super.key,
});
final void Function() onAddCommunity;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -27,10 +26,43 @@ class SpaceManagementSidebarHeader extends StatelessWidget {
), ),
), ),
SpaceManagementSidebarAddCommunityButton( 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( return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [