From 67e94ab0bd0d7dcf2f442af1e7372fc721ba1b3a Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 2 Jul 2025 17:46:57 +0100 Subject: [PATCH 01/12] feat(content-management): implement pagination for headlines, categories, and sources --- .../bloc/content_management_bloc.dart | 52 ++++++++++++++----- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/lib/content_management/bloc/content_management_bloc.dart b/lib/content_management/bloc/content_management_bloc.dart index f725ed8..ff0bf34 100644 --- a/lib/content_management/bloc/content_management_bloc.dart +++ b/lib/content_management/bloc/content_management_bloc.dart @@ -1,6 +1,7 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; +import 'package:ht_dashboard/shared/constants/pagination_constants.dart'; import 'package:ht_shared/ht_shared.dart'; part 'content_management_event.dart'; @@ -60,6 +61,9 @@ class ContentManagementBloc ) async { emit(state.copyWith(headlinesStatus: ContentManagementStatus.loading)); try { + final isPaginating = event.startAfterId != null; + final previousHeadlines = isPaginating ? state.headlines : []; + final paginatedHeadlines = await _headlinesRepository.readAll( startAfterId: event.startAfterId, limit: event.limit, @@ -67,7 +71,7 @@ class ContentManagementBloc emit( state.copyWith( headlinesStatus: ContentManagementStatus.success, - headlines: paginatedHeadlines.items, + headlines: [...previousHeadlines, ...paginatedHeadlines.items], headlinesCursor: paginatedHeadlines.cursor, headlinesHasMore: paginatedHeadlines.hasMore, ), @@ -97,7 +101,9 @@ class ContentManagementBloc try { await _headlinesRepository.create(item: event.headline); // Reload headlines after creation - add(const LoadHeadlinesRequested()); + add( + const LoadHeadlinesRequested(limit: kDefaultRowsPerPage), + ); } on HtHttpException catch (e) { emit( state.copyWith( @@ -123,7 +129,9 @@ class ContentManagementBloc try { await _headlinesRepository.update(id: event.id, item: event.headline); // Reload headlines after update - add(const LoadHeadlinesRequested()); + add( + const LoadHeadlinesRequested(limit: kDefaultRowsPerPage), + ); } on HtHttpException catch (e) { emit( state.copyWith( @@ -149,7 +157,9 @@ class ContentManagementBloc try { await _headlinesRepository.delete(id: event.id); // Reload headlines after deletion - add(const LoadHeadlinesRequested()); + add( + const LoadHeadlinesRequested(limit: kDefaultRowsPerPage), + ); } on HtHttpException catch (e) { emit( state.copyWith( @@ -173,6 +183,9 @@ class ContentManagementBloc ) async { emit(state.copyWith(categoriesStatus: ContentManagementStatus.loading)); try { + final isPaginating = event.startAfterId != null; + final previousCategories = isPaginating ? state.categories : []; + final paginatedCategories = await _categoriesRepository.readAll( startAfterId: event.startAfterId, limit: event.limit, @@ -180,7 +193,7 @@ class ContentManagementBloc emit( state.copyWith( categoriesStatus: ContentManagementStatus.success, - categories: paginatedCategories.items, + categories: [...previousCategories, ...paginatedCategories.items], categoriesCursor: paginatedCategories.cursor, categoriesHasMore: paginatedCategories.hasMore, ), @@ -210,7 +223,9 @@ class ContentManagementBloc try { await _categoriesRepository.create(item: event.category); // Reload categories after creation - add(const LoadCategoriesRequested()); + add( + const LoadCategoriesRequested(limit: kDefaultRowsPerPage), + ); } on HtHttpException catch (e) { emit( state.copyWith( @@ -236,7 +251,9 @@ class ContentManagementBloc try { await _categoriesRepository.update(id: event.id, item: event.category); // Reload categories after update - add(const LoadCategoriesRequested()); + add( + const LoadCategoriesRequested(limit: kDefaultRowsPerPage), + ); } on HtHttpException catch (e) { emit( state.copyWith( @@ -262,7 +279,9 @@ class ContentManagementBloc try { await _categoriesRepository.delete(id: event.id); // Reload categories after deletion - add(const LoadCategoriesRequested()); + add( + const LoadCategoriesRequested(limit: kDefaultRowsPerPage), + ); } on HtHttpException catch (e) { emit( state.copyWith( @@ -286,6 +305,9 @@ class ContentManagementBloc ) async { emit(state.copyWith(sourcesStatus: ContentManagementStatus.loading)); try { + final isPaginating = event.startAfterId != null; + final previousSources = isPaginating ? state.sources : []; + final paginatedSources = await _sourcesRepository.readAll( startAfterId: event.startAfterId, limit: event.limit, @@ -293,7 +315,7 @@ class ContentManagementBloc emit( state.copyWith( sourcesStatus: ContentManagementStatus.success, - sources: paginatedSources.items, + sources: [...previousSources, ...paginatedSources.items], sourcesCursor: paginatedSources.cursor, sourcesHasMore: paginatedSources.hasMore, ), @@ -323,7 +345,9 @@ class ContentManagementBloc try { await _sourcesRepository.create(item: event.source); // Reload sources after creation - add(const LoadSourcesRequested()); + add( + const LoadSourcesRequested(limit: kDefaultRowsPerPage), + ); } on HtHttpException catch (e) { emit( state.copyWith( @@ -349,7 +373,9 @@ class ContentManagementBloc try { await _sourcesRepository.update(id: event.id, item: event.source); // Reload sources after update - add(const LoadSourcesRequested()); + add( + const LoadSourcesRequested(limit: kDefaultRowsPerPage), + ); } on HtHttpException catch (e) { emit( state.copyWith( @@ -375,7 +401,9 @@ class ContentManagementBloc try { await _sourcesRepository.delete(id: event.id); // Reload sources after deletion - add(const LoadSourcesRequested()); + add( + const LoadSourcesRequested(limit: kDefaultRowsPerPage), + ); } on HtHttpException catch (e) { emit( state.copyWith( From 0017d1d5aaf17797862e073459db693213a7178d Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 2 Jul 2025 17:47:12 +0100 Subject: [PATCH 02/12] feat(content-management): enhance categories pagination with dynamic row count and loading state --- .../view/categories_page.dart | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/lib/content_management/view/categories_page.dart b/lib/content_management/view/categories_page.dart index 34d0605..ef6e872 100644 --- a/lib/content_management/view/categories_page.dart +++ b/lib/content_management/view/categories_page.dart @@ -6,6 +6,7 @@ import 'package:ht_dashboard/content_management/bloc/content_management_bloc.dar import 'package:ht_dashboard/l10n/app_localizations.dart'; import 'package:ht_dashboard/l10n/l10n.dart'; import 'package:ht_dashboard/router/routes.dart'; +import 'package:ht_dashboard/shared/constants/pagination_constants.dart'; import 'package:ht_dashboard/shared/constants/app_spacing.dart'; import 'package:ht_dashboard/shared/widgets/failure_state_widget.dart'; import 'package:ht_dashboard/shared/widgets/loading_state_widget.dart'; @@ -23,14 +24,12 @@ class CategoriesPage extends StatefulWidget { } class _CategoriesPageState extends State { - static const int _rowsPerPage = 10; - @override void initState() { super.initState(); context.read().add( - const LoadCategoriesRequested(limit: _rowsPerPage), - ); + const LoadCategoriesRequested(limit: kDefaultRowsPerPage), + ); } @override @@ -53,8 +52,8 @@ class _CategoriesPageState extends State { return FailureStateWidget( message: state.errorMessage ?? l10n.unknownError, onRetry: () => context.read().add( - const LoadCategoriesRequested(limit: _rowsPerPage), - ), + const LoadCategoriesRequested(limit: kDefaultRowsPerPage), + ), ); } @@ -82,20 +81,21 @@ class _CategoriesPageState extends State { source: _CategoriesDataSource( context: context, categories: state.categories, + hasMore: state.categoriesHasMore, l10n: l10n, ), - rowsPerPage: _rowsPerPage, - availableRowsPerPage: const [_rowsPerPage], + rowsPerPage: kDefaultRowsPerPage, + availableRowsPerPage: const [kDefaultRowsPerPage], onPageChanged: (pageIndex) { - final newOffset = pageIndex * _rowsPerPage; + final newOffset = pageIndex * kDefaultRowsPerPage; if (newOffset >= state.categories.length && state.categoriesHasMore) { context.read().add( - LoadCategoriesRequested( - startAfterId: state.categoriesCursor, - limit: _rowsPerPage, - ), - ); + LoadCategoriesRequested( + startAfterId: state.categoriesCursor, + limit: kDefaultRowsPerPage, + ), + ); } }, empty: Center(child: Text(l10n.noCategoriesFound)), @@ -117,16 +117,22 @@ class _CategoriesDataSource extends DataTableSource { _CategoriesDataSource({ required this.context, required this.categories, + required this.hasMore, required this.l10n, }); final BuildContext context; final List categories; + final bool hasMore; final AppLocalizations l10n; @override DataRow? getRow(int index) { if (index >= categories.length) { + // This can happen if hasMore is true and the user is on the last page. + // The table will try to build one extra row that is out of bounds. + // We return null to signify the end of the available data. + // The onPageChanged callback will handle fetching more data. return null; } final category = categories[index]; @@ -152,8 +158,8 @@ class _CategoriesDataSource extends DataTableSource { onPressed: () { // Dispatch delete event context.read().add( - DeleteCategoryRequested(category.id), - ); + DeleteCategoryRequested(category.id), + ); }, ), ], @@ -164,10 +170,18 @@ class _CategoriesDataSource extends DataTableSource { } @override - bool get isRowCountApproximate => false; + bool get isRowCountApproximate => true; @override - int get rowCount => categories.length; + int get rowCount { + // If we have more items to fetch, we add 1 to the current length. + // This signals to PaginatedDataTable2 that there is at least one more page, + // which enables the 'next page' button. + if (hasMore) { + return categories.length + 1; + } + return categories.length; + } @override int get selectedRowCount => 0; From 9a0ca8037a30a80d9e7c81cf28a218a50c367a34 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 2 Jul 2025 17:47:19 +0100 Subject: [PATCH 03/12] feat(content-management): update LoadCategoriesRequested to include default row limit --- lib/content_management/view/create_category_page.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/content_management/view/create_category_page.dart b/lib/content_management/view/create_category_page.dart index 2602094..f2e2e77 100644 --- a/lib/content_management/view/create_category_page.dart +++ b/lib/content_management/view/create_category_page.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:ht_dashboard/content_management/bloc/content_management_bloc.dart'; import 'package:ht_dashboard/content_management/bloc/create_category/create_category_bloc.dart'; import 'package:ht_dashboard/l10n/l10n.dart'; +import 'package:ht_dashboard/shared/constants/pagination_constants.dart'; import 'package:ht_dashboard/shared/shared.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_shared/ht_shared.dart'; @@ -82,7 +83,9 @@ class _CreateCategoryViewState extends State<_CreateCategoryView> { ), ); context.read().add( - const LoadCategoriesRequested(), + const LoadCategoriesRequested( + limit: kDefaultRowsPerPage, + ), ); context.pop(); } From 6b4961d921b223801113bd38e6164047c1df95b4 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 2 Jul 2025 17:47:24 +0100 Subject: [PATCH 04/12] feat(content-management): update LoadHeadlinesRequested to include default row limit --- lib/content_management/view/create_headline_page.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/content_management/view/create_headline_page.dart b/lib/content_management/view/create_headline_page.dart index 18e4cfc..cfbd1ae 100644 --- a/lib/content_management/view/create_headline_page.dart +++ b/lib/content_management/view/create_headline_page.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:ht_dashboard/content_management/bloc/content_management_bloc.dart'; import 'package:ht_dashboard/content_management/bloc/create_headline/create_headline_bloc.dart'; import 'package:ht_dashboard/l10n/l10n.dart'; +import 'package:ht_dashboard/shared/constants/pagination_constants.dart'; import 'package:ht_dashboard/shared/shared.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_shared/ht_shared.dart'; @@ -84,7 +85,9 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> { ), ); context.read().add( - const LoadHeadlinesRequested(), + const LoadHeadlinesRequested( + limit: kDefaultRowsPerPage, + ), ); context.pop(); } From 2bb5cce77c2c7fd3891b56c1cc39f6297c793389 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 2 Jul 2025 17:47:29 +0100 Subject: [PATCH 05/12] feat(content-management): update LoadSourcesRequested to include default row limit --- lib/content_management/view/create_source_page.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/content_management/view/create_source_page.dart b/lib/content_management/view/create_source_page.dart index c5c70d6..1239b33 100644 --- a/lib/content_management/view/create_source_page.dart +++ b/lib/content_management/view/create_source_page.dart @@ -5,6 +5,7 @@ import 'package:ht_dashboard/content_management/bloc/content_management_bloc.dar import 'package:ht_dashboard/content_management/bloc/create_source/create_source_bloc.dart'; import 'package:ht_dashboard/content_management/bloc/edit_source/edit_source_bloc.dart'; import 'package:ht_dashboard/l10n/l10n.dart'; +import 'package:ht_dashboard/shared/constants/pagination_constants.dart'; import 'package:ht_dashboard/shared/shared.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_shared/ht_shared.dart'; @@ -82,7 +83,9 @@ class _CreateSourceViewState extends State<_CreateSourceView> { SnackBar(content: Text(l10n.sourceCreatedSuccessfully)), ); context.read().add( - const LoadSourcesRequested(), + const LoadSourcesRequested( + limit: kDefaultRowsPerPage, + ), ); context.pop(); } From e6ace786335140ac7ec0ceecd701a1565e74109b Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 2 Jul 2025 17:47:34 +0100 Subject: [PATCH 06/12] feat(content-management): update LoadCategoriesRequested to include default row limit --- lib/content_management/view/edit_category_page.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/content_management/view/edit_category_page.dart b/lib/content_management/view/edit_category_page.dart index 1c977a5..902b4e0 100644 --- a/lib/content_management/view/edit_category_page.dart +++ b/lib/content_management/view/edit_category_page.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:ht_dashboard/content_management/bloc/content_management_bloc.dart'; import 'package:ht_dashboard/content_management/bloc/edit_category/edit_category_bloc.dart'; import 'package:ht_dashboard/l10n/l10n.dart'; +import 'package:ht_dashboard/shared/constants/pagination_constants.dart'; import 'package:ht_dashboard/shared/shared.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_shared/ht_shared.dart'; @@ -108,7 +109,9 @@ class _EditCategoryViewState extends State<_EditCategoryView> { const SnackBar(content: Text('Category updated successfully.')), ); context.read().add( - const LoadCategoriesRequested(), + const LoadCategoriesRequested( + limit: kDefaultRowsPerPage, + ), ); context.pop(); } From 4e90352055125b02b59ff5662e68050bb43b34e2 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 2 Jul 2025 17:47:40 +0100 Subject: [PATCH 07/12] feat(content-management): update LoadHeadlinesRequested to include default row limit --- lib/content_management/view/edit_headline_page.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/content_management/view/edit_headline_page.dart b/lib/content_management/view/edit_headline_page.dart index e78394b..83922ce 100644 --- a/lib/content_management/view/edit_headline_page.dart +++ b/lib/content_management/view/edit_headline_page.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:ht_dashboard/content_management/bloc/content_management_bloc.dart'; import 'package:ht_dashboard/content_management/bloc/edit_headline/edit_headline_bloc.dart'; import 'package:ht_dashboard/l10n/l10n.dart'; +import 'package:ht_dashboard/shared/constants/pagination_constants.dart'; import 'package:ht_dashboard/shared/shared.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_shared/ht_shared.dart'; @@ -114,7 +115,9 @@ class _EditHeadlineViewState extends State<_EditHeadlineView> { ), ); context.read().add( - const LoadHeadlinesRequested(), + const LoadHeadlinesRequested( + limit: kDefaultRowsPerPage, + ), ); context.pop(); } From 2d7f1115335f90789639e5d84a1fc1e616184f4b Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 2 Jul 2025 17:47:44 +0100 Subject: [PATCH 08/12] feat(content-management): update LoadSourcesRequested to include default row limit --- lib/content_management/view/edit_source_page.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/content_management/view/edit_source_page.dart b/lib/content_management/view/edit_source_page.dart index 387b65c..db148b7 100644 --- a/lib/content_management/view/edit_source_page.dart +++ b/lib/content_management/view/edit_source_page.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:ht_dashboard/content_management/bloc/content_management_bloc.dart'; import 'package:ht_dashboard/content_management/bloc/edit_source/edit_source_bloc.dart'; import 'package:ht_dashboard/l10n/l10n.dart'; +import 'package:ht_dashboard/shared/constants/pagination_constants.dart'; import 'package:ht_dashboard/shared/shared.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_shared/ht_shared.dart'; @@ -111,7 +112,9 @@ class _EditSourceViewState extends State<_EditSourceView> { SnackBar(content: Text(l10n.sourceUpdatedSuccessfully)), ); context.read().add( - const LoadSourcesRequested(), + const LoadSourcesRequested( + limit: kDefaultRowsPerPage, + ), ); context.pop(); } From 5e906da731f3f5bf55ffedba863641bb00f5e8b1 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 2 Jul 2025 17:47:51 +0100 Subject: [PATCH 09/12] feat(content-management): update LoadHeadlinesRequested to use kDefaultRowsPerPage and improve pagination handling --- .../view/headlines_page.dart | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/lib/content_management/view/headlines_page.dart b/lib/content_management/view/headlines_page.dart index 51d0339..2435bdb 100644 --- a/lib/content_management/view/headlines_page.dart +++ b/lib/content_management/view/headlines_page.dart @@ -6,6 +6,7 @@ import 'package:ht_dashboard/content_management/bloc/content_management_bloc.dar import 'package:ht_dashboard/l10n/app_localizations.dart'; // Corrected import import 'package:ht_dashboard/l10n/l10n.dart'; import 'package:ht_dashboard/router/routes.dart'; +import 'package:ht_dashboard/shared/constants/pagination_constants.dart'; import 'package:ht_dashboard/shared/constants/app_spacing.dart'; import 'package:ht_dashboard/shared/utils/date_formatter.dart'; import 'package:ht_dashboard/shared/widgets/failure_state_widget.dart'; @@ -24,14 +25,12 @@ class HeadlinesPage extends StatefulWidget { } class _HeadlinesPageState extends State { - static const int _rowsPerPage = 10; - @override void initState() { super.initState(); context.read().add( - const LoadHeadlinesRequested(limit: _rowsPerPage), - ); + const LoadHeadlinesRequested(limit: kDefaultRowsPerPage), + ); } @override @@ -54,8 +53,8 @@ class _HeadlinesPageState extends State { return FailureStateWidget( message: state.errorMessage ?? l10n.unknownError, onRetry: () => context.read().add( - const LoadHeadlinesRequested(limit: _rowsPerPage), - ), + const LoadHeadlinesRequested(limit: kDefaultRowsPerPage), + ), ); } @@ -87,20 +86,21 @@ class _HeadlinesPageState extends State { source: _HeadlinesDataSource( context: context, headlines: state.headlines, + hasMore: state.headlinesHasMore, l10n: l10n, ), - rowsPerPage: _rowsPerPage, - availableRowsPerPage: const [_rowsPerPage], + rowsPerPage: kDefaultRowsPerPage, + availableRowsPerPage: const [kDefaultRowsPerPage], onPageChanged: (pageIndex) { - final newOffset = pageIndex * _rowsPerPage; + final newOffset = pageIndex * kDefaultRowsPerPage; if (newOffset >= state.headlines.length && state.headlinesHasMore) { context.read().add( - LoadHeadlinesRequested( - startAfterId: state.headlinesCursor, - limit: _rowsPerPage, - ), - ); + LoadHeadlinesRequested( + startAfterId: state.headlinesCursor, + limit: kDefaultRowsPerPage, + ), + ); } }, empty: Center(child: Text(l10n.noHeadlinesFound)), @@ -122,16 +122,22 @@ class _HeadlinesDataSource extends DataTableSource { _HeadlinesDataSource({ required this.context, required this.headlines, + required this.hasMore, required this.l10n, }); final BuildContext context; final List headlines; + final bool hasMore; final AppLocalizations l10n; @override DataRow? getRow(int index) { if (index >= headlines.length) { + // This can happen if hasMore is true and the user is on the last page. + // The table will try to build one extra row that is out of bounds. + // We return null to signify the end of the available data. + // The onPageChanged callback will handle fetching more data. return null; } final headline = headlines[index]; @@ -164,8 +170,8 @@ class _HeadlinesDataSource extends DataTableSource { onPressed: () { // Dispatch delete event context.read().add( - DeleteHeadlineRequested(headline.id), - ); + DeleteHeadlineRequested(headline.id), + ); }, ), ], @@ -176,10 +182,18 @@ class _HeadlinesDataSource extends DataTableSource { } @override - bool get isRowCountApproximate => false; + bool get isRowCountApproximate => true; @override - int get rowCount => headlines.length; + int get rowCount { + // If we have more items to fetch, we add 1 to the current length. + // This signals to PaginatedDataTable2 that there is at least one more page, + // which enables the 'next page' button. + if (hasMore) { + return headlines.length + 1; + } + return headlines.length; + } @override int get selectedRowCount => 0; From 43e715585ace966254256f0a6dfe5dbd7e739af3 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 2 Jul 2025 17:47:56 +0100 Subject: [PATCH 10/12] feat(content-management): update LoadSourcesRequested to use kDefaultRowsPerPage and improve pagination handling --- lib/content_management/view/sources_page.dart | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/lib/content_management/view/sources_page.dart b/lib/content_management/view/sources_page.dart index 93623e2..65e031d 100644 --- a/lib/content_management/view/sources_page.dart +++ b/lib/content_management/view/sources_page.dart @@ -6,6 +6,7 @@ import 'package:ht_dashboard/content_management/bloc/content_management_bloc.dar import 'package:ht_dashboard/l10n/app_localizations.dart'; import 'package:ht_dashboard/l10n/l10n.dart'; import 'package:ht_dashboard/router/routes.dart'; +import 'package:ht_dashboard/shared/constants/pagination_constants.dart'; import 'package:ht_dashboard/shared/constants/app_spacing.dart'; import 'package:ht_dashboard/shared/widgets/failure_state_widget.dart'; import 'package:ht_dashboard/shared/widgets/loading_state_widget.dart'; @@ -23,13 +24,11 @@ class SourcesPage extends StatefulWidget { } class _SourcesPageState extends State { - static const int _rowsPerPage = 10; - @override void initState() { super.initState(); context.read().add( - const LoadSourcesRequested(limit: _rowsPerPage), + const LoadSourcesRequested(limit: kDefaultRowsPerPage), ); } @@ -53,7 +52,7 @@ class _SourcesPageState extends State { return FailureStateWidget( message: state.errorMessage ?? l10n.unknownError, onRetry: () => context.read().add( - const LoadSourcesRequested(limit: _rowsPerPage), + const LoadSourcesRequested(limit: kDefaultRowsPerPage), ), ); } @@ -86,17 +85,18 @@ class _SourcesPageState extends State { source: _SourcesDataSource( context: context, sources: state.sources, + hasMore: state.sourcesHasMore, l10n: l10n, ), - rowsPerPage: _rowsPerPage, - availableRowsPerPage: const [_rowsPerPage], + rowsPerPage: kDefaultRowsPerPage, + availableRowsPerPage: const [kDefaultRowsPerPage], onPageChanged: (pageIndex) { - final newOffset = pageIndex * _rowsPerPage; + final newOffset = pageIndex * kDefaultRowsPerPage; if (newOffset >= state.sources.length && state.sourcesHasMore) { context.read().add( LoadSourcesRequested( startAfterId: state.sourcesCursor, - limit: _rowsPerPage, + limit: kDefaultRowsPerPage, ), ); } @@ -120,16 +120,22 @@ class _SourcesDataSource extends DataTableSource { _SourcesDataSource({ required this.context, required this.sources, + required this.hasMore, required this.l10n, }); final BuildContext context; final List sources; + final bool hasMore; final AppLocalizations l10n; @override DataRow? getRow(int index) { if (index >= sources.length) { + // This can happen if hasMore is true and the user is on the last page. + // The table will try to build one extra row that is out of bounds. + // We return null to signify the end of the available data. + // The onPageChanged callback will handle fetching more data. return null; } final source = sources[index]; @@ -168,10 +174,18 @@ class _SourcesDataSource extends DataTableSource { } @override - bool get isRowCountApproximate => false; + bool get isRowCountApproximate => true; @override - int get rowCount => sources.length; + int get rowCount { + // If we have more items to fetch, we add 1 to the current length. + // This signals to PaginatedDataTable2 that there is at least one more page, + // which enables the 'next page' button. + if (hasMore) { + return sources.length + 1; + } + return sources.length; + } @override int get selectedRowCount => 0; From fa42e403511b80941791d7452948f4b47318c71b Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 2 Jul 2025 17:48:00 +0100 Subject: [PATCH 11/12] feat(constants): add kDefaultRowsPerPage for pagination handling --- lib/shared/constants/pagination_constants.dart | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 lib/shared/constants/pagination_constants.dart diff --git a/lib/shared/constants/pagination_constants.dart b/lib/shared/constants/pagination_constants.dart new file mode 100644 index 0000000..5f6b2a9 --- /dev/null +++ b/lib/shared/constants/pagination_constants.dart @@ -0,0 +1,2 @@ +/// Default number of rows to display per page in paginated data tables. +const int kDefaultRowsPerPage = 10; From 2a4a70b22bce992f8c9e4cb54ae591212a19caee Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 2 Jul 2025 18:09:45 +0100 Subject: [PATCH 12/12] feat(content-management): enhance pagination handling with loading state indicators for categories, headlines, and sources --- .../view/categories_page.dart | 21 +++++--- .../view/headlines_page.dart | 21 +++++--- lib/content_management/view/sources_page.dart | 49 ++++++++++++------- 3 files changed, 62 insertions(+), 29 deletions(-) diff --git a/lib/content_management/view/categories_page.dart b/lib/content_management/view/categories_page.dart index ef6e872..4cfc6cc 100644 --- a/lib/content_management/view/categories_page.dart +++ b/lib/content_management/view/categories_page.dart @@ -81,6 +81,7 @@ class _CategoriesPageState extends State { source: _CategoriesDataSource( context: context, categories: state.categories, + isLoading: state.categoriesStatus == ContentManagementStatus.loading, hasMore: state.categoriesHasMore, l10n: l10n, ), @@ -89,7 +90,8 @@ class _CategoriesPageState extends State { onPageChanged: (pageIndex) { final newOffset = pageIndex * kDefaultRowsPerPage; if (newOffset >= state.categories.length && - state.categoriesHasMore) { + state.categoriesHasMore && + state.categoriesStatus != ContentManagementStatus.loading) { context.read().add( LoadCategoriesRequested( startAfterId: state.categoriesCursor, @@ -117,12 +119,14 @@ class _CategoriesDataSource extends DataTableSource { _CategoriesDataSource({ required this.context, required this.categories, + required this.isLoading, required this.hasMore, required this.l10n, }); final BuildContext context; final List categories; + final bool isLoading; final bool hasMore; final AppLocalizations l10n; @@ -130,9 +134,12 @@ class _CategoriesDataSource extends DataTableSource { DataRow? getRow(int index) { if (index >= categories.length) { // This can happen if hasMore is true and the user is on the last page. - // The table will try to build one extra row that is out of bounds. - // We return null to signify the end of the available data. - // The onPageChanged callback will handle fetching more data. + // If we are loading, show a spinner. Otherwise, we've reached the end. + if (isLoading) { + return DataRow2( + cells: List.generate(3, (_) => const DataCell(Center(child: CircularProgressIndicator()))), + ); + } return null; } final category = categories[index]; @@ -170,7 +177,7 @@ class _CategoriesDataSource extends DataTableSource { } @override - bool get isRowCountApproximate => true; + bool get isRowCountApproximate => hasMore; @override int get rowCount { @@ -178,7 +185,9 @@ class _CategoriesDataSource extends DataTableSource { // This signals to PaginatedDataTable2 that there is at least one more page, // which enables the 'next page' button. if (hasMore) { - return categories.length + 1; + // When loading, we show an extra row for the spinner. + // Otherwise, we just indicate that there are more rows. + return isLoading ? categories.length + 1 : categories.length + kDefaultRowsPerPage; } return categories.length; } diff --git a/lib/content_management/view/headlines_page.dart b/lib/content_management/view/headlines_page.dart index 2435bdb..e9d3cc7 100644 --- a/lib/content_management/view/headlines_page.dart +++ b/lib/content_management/view/headlines_page.dart @@ -86,6 +86,7 @@ class _HeadlinesPageState extends State { source: _HeadlinesDataSource( context: context, headlines: state.headlines, + isLoading: state.headlinesStatus == ContentManagementStatus.loading, hasMore: state.headlinesHasMore, l10n: l10n, ), @@ -94,7 +95,8 @@ class _HeadlinesPageState extends State { onPageChanged: (pageIndex) { final newOffset = pageIndex * kDefaultRowsPerPage; if (newOffset >= state.headlines.length && - state.headlinesHasMore) { + state.headlinesHasMore && + state.headlinesStatus != ContentManagementStatus.loading) { context.read().add( LoadHeadlinesRequested( startAfterId: state.headlinesCursor, @@ -122,12 +124,14 @@ class _HeadlinesDataSource extends DataTableSource { _HeadlinesDataSource({ required this.context, required this.headlines, + required this.isLoading, required this.hasMore, required this.l10n, }); final BuildContext context; final List headlines; + final bool isLoading; final bool hasMore; final AppLocalizations l10n; @@ -135,9 +139,12 @@ class _HeadlinesDataSource extends DataTableSource { DataRow? getRow(int index) { if (index >= headlines.length) { // This can happen if hasMore is true and the user is on the last page. - // The table will try to build one extra row that is out of bounds. - // We return null to signify the end of the available data. - // The onPageChanged callback will handle fetching more data. + // If we are loading, show a spinner. Otherwise, we've reached the end. + if (isLoading) { + return DataRow2( + cells: List.generate(4, (_) => const DataCell(Center(child: CircularProgressIndicator()))), + ); + } return null; } final headline = headlines[index]; @@ -182,7 +189,7 @@ class _HeadlinesDataSource extends DataTableSource { } @override - bool get isRowCountApproximate => true; + bool get isRowCountApproximate => hasMore; @override int get rowCount { @@ -190,7 +197,9 @@ class _HeadlinesDataSource extends DataTableSource { // This signals to PaginatedDataTable2 that there is at least one more page, // which enables the 'next page' button. if (hasMore) { - return headlines.length + 1; + // When loading, we show an extra row for the spinner. + // Otherwise, we just indicate that there are more rows. + return isLoading ? headlines.length + 1 : headlines.length + kDefaultRowsPerPage; } return headlines.length; } diff --git a/lib/content_management/view/sources_page.dart b/lib/content_management/view/sources_page.dart index 65e031d..e3d42c1 100644 --- a/lib/content_management/view/sources_page.dart +++ b/lib/content_management/view/sources_page.dart @@ -28,8 +28,8 @@ class _SourcesPageState extends State { void initState() { super.initState(); context.read().add( - const LoadSourcesRequested(limit: kDefaultRowsPerPage), - ); + const LoadSourcesRequested(limit: kDefaultRowsPerPage), + ); } @override @@ -52,8 +52,8 @@ class _SourcesPageState extends State { return FailureStateWidget( message: state.errorMessage ?? l10n.unknownError, onRetry: () => context.read().add( - const LoadSourcesRequested(limit: kDefaultRowsPerPage), - ), + const LoadSourcesRequested(limit: kDefaultRowsPerPage), + ), ); } @@ -85,6 +85,7 @@ class _SourcesPageState extends State { source: _SourcesDataSource( context: context, sources: state.sources, + isLoading: state.sourcesStatus == ContentManagementStatus.loading, hasMore: state.sourcesHasMore, l10n: l10n, ), @@ -92,13 +93,15 @@ class _SourcesPageState extends State { availableRowsPerPage: const [kDefaultRowsPerPage], onPageChanged: (pageIndex) { final newOffset = pageIndex * kDefaultRowsPerPage; - if (newOffset >= state.sources.length && state.sourcesHasMore) { + if (newOffset >= state.sources.length && + state.sourcesHasMore && + state.sourcesStatus != ContentManagementStatus.loading) { context.read().add( - LoadSourcesRequested( - startAfterId: state.sourcesCursor, - limit: kDefaultRowsPerPage, - ), - ); + LoadSourcesRequested( + startAfterId: state.sourcesCursor, + limit: kDefaultRowsPerPage, + ), + ); } }, empty: Center(child: Text(l10n.noSourcesFound)), @@ -120,12 +123,14 @@ class _SourcesDataSource extends DataTableSource { _SourcesDataSource({ required this.context, required this.sources, + required this.isLoading, required this.hasMore, required this.l10n, }); final BuildContext context; final List sources; + final bool isLoading; final bool hasMore; final AppLocalizations l10n; @@ -133,9 +138,15 @@ class _SourcesDataSource extends DataTableSource { DataRow? getRow(int index) { if (index >= sources.length) { // This can happen if hasMore is true and the user is on the last page. - // The table will try to build one extra row that is out of bounds. - // We return null to signify the end of the available data. - // The onPageChanged callback will handle fetching more data. + // If we are loading, show a spinner. Otherwise, we've reached the end. + if (isLoading) { + return DataRow2( + cells: List.generate( + 4, + (_) => const DataCell(Center(child: CircularProgressIndicator())), + ), + ); + } return null; } final source = sources[index]; @@ -162,8 +173,8 @@ class _SourcesDataSource extends DataTableSource { onPressed: () { // Dispatch delete event context.read().add( - DeleteSourceRequested(source.id), - ); + DeleteSourceRequested(source.id), + ); }, ), ], @@ -174,7 +185,7 @@ class _SourcesDataSource extends DataTableSource { } @override - bool get isRowCountApproximate => true; + bool get isRowCountApproximate => hasMore; @override int get rowCount { @@ -182,7 +193,11 @@ class _SourcesDataSource extends DataTableSource { // This signals to PaginatedDataTable2 that there is at least one more page, // which enables the 'next page' button. if (hasMore) { - return sources.length + 1; + // When loading, we show an extra row for the spinner. + // Otherwise, we just indicate that there are more rows. + return isLoading + ? sources.length + 1 + : sources.length + kDefaultRowsPerPage; } return sources.length; }