Skip to content

Fix data fetching in content management #17

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jul 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 40 additions & 12 deletions lib/content_management/bloc/content_management_bloc.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -60,14 +61,17 @@ class ContentManagementBloc
) async {
emit(state.copyWith(headlinesStatus: ContentManagementStatus.loading));
try {
final isPaginating = event.startAfterId != null;
final previousHeadlines = isPaginating ? state.headlines : <Headline>[];

final paginatedHeadlines = await _headlinesRepository.readAll(
startAfterId: event.startAfterId,
limit: event.limit,
);
emit(
state.copyWith(
headlinesStatus: ContentManagementStatus.success,
headlines: paginatedHeadlines.items,
headlines: [...previousHeadlines, ...paginatedHeadlines.items],
headlinesCursor: paginatedHeadlines.cursor,
headlinesHasMore: paginatedHeadlines.hasMore,
),
Expand Down Expand Up @@ -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),
);
Comment on lines +104 to +106

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Reloading the first page after a CUD operation can be a poor user experience if the user has paginated. Consider optimistic updates or invalidation with smart refetching to maintain the user's position.

} on HtHttpException catch (e) {
emit(
state.copyWith(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -173,14 +183,17 @@ class ContentManagementBloc
) async {
emit(state.copyWith(categoriesStatus: ContentManagementStatus.loading));
try {
final isPaginating = event.startAfterId != null;
final previousCategories = isPaginating ? state.categories : <Category>[];

final paginatedCategories = await _categoriesRepository.readAll(
startAfterId: event.startAfterId,
limit: event.limit,
);
emit(
state.copyWith(
categoriesStatus: ContentManagementStatus.success,
categories: paginatedCategories.items,
categories: [...previousCategories, ...paginatedCategories.items],
categoriesCursor: paginatedCategories.cursor,
categoriesHasMore: paginatedCategories.hasMore,
),
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -286,14 +305,17 @@ class ContentManagementBloc
) async {
emit(state.copyWith(sourcesStatus: ContentManagementStatus.loading));
try {
final isPaginating = event.startAfterId != null;
final previousSources = isPaginating ? state.sources : <Source>[];

final paginatedSources = await _sourcesRepository.readAll(
startAfterId: event.startAfterId,
limit: event.limit,
);
emit(
state.copyWith(
sourcesStatus: ContentManagementStatus.success,
sources: paginatedSources.items,
sources: [...previousSources, ...paginatedSources.items],
sourcesCursor: paginatedSources.cursor,
sourcesHasMore: paginatedSources.hasMore,
),
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand Down
61 changes: 42 additions & 19 deletions lib/content_management/view/categories_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -23,14 +24,12 @@ class CategoriesPage extends StatefulWidget {
}

class _CategoriesPageState extends State<CategoriesPage> {
static const int _rowsPerPage = 10;

@override
void initState() {
super.initState();
context.read<ContentManagementBloc>().add(
const LoadCategoriesRequested(limit: _rowsPerPage),
);
const LoadCategoriesRequested(limit: kDefaultRowsPerPage),
);
}

@override
Expand All @@ -53,8 +52,8 @@ class _CategoriesPageState extends State<CategoriesPage> {
return FailureStateWidget(
message: state.errorMessage ?? l10n.unknownError,
onRetry: () => context.read<ContentManagementBloc>().add(
const LoadCategoriesRequested(limit: _rowsPerPage),
),
const LoadCategoriesRequested(limit: kDefaultRowsPerPage),
),
);
}

Expand Down Expand Up @@ -82,20 +81,23 @@ class _CategoriesPageState extends State<CategoriesPage> {
source: _CategoriesDataSource(
context: context,
categories: state.categories,
isLoading: state.categoriesStatus == ContentManagementStatus.loading,
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) {
state.categoriesHasMore &&
state.categoriesStatus != ContentManagementStatus.loading) {
context.read<ContentManagementBloc>().add(
LoadCategoriesRequested(
startAfterId: state.categoriesCursor,
limit: _rowsPerPage,
),
);
LoadCategoriesRequested(
startAfterId: state.categoriesCursor,
limit: kDefaultRowsPerPage,
),
);
}
},
empty: Center(child: Text(l10n.noCategoriesFound)),
Expand All @@ -117,16 +119,27 @@ 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<Category> categories;
final bool isLoading;
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.
// 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];
Expand All @@ -152,8 +165,8 @@ class _CategoriesDataSource extends DataTableSource {
onPressed: () {
// Dispatch delete event
context.read<ContentManagementBloc>().add(
DeleteCategoryRequested(category.id),
);
DeleteCategoryRequested(category.id),
);
},
),
],
Expand All @@ -164,10 +177,20 @@ class _CategoriesDataSource extends DataTableSource {
}

@override
bool get isRowCountApproximate => false;
bool get isRowCountApproximate => hasMore;

@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) {
// 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;
}
Comment on lines +183 to +193

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Returning categories.length + kDefaultRowsPerPage when hasMore is true but not loading will cause the PaginatedDataTable2 to render kDefaultRowsPerPage empty rows at the end of the table. To simply signal that there are more pages, you only need to return a rowCount that is greater than the current number of items. A rowCount of categories.length + 1 is sufficient to enable the 'next page' button when isRowCountApproximate is true.

  int get rowCount {
    if (hasMore) {
      return categories.length + 1;
    }
    return categories.length;
  }


@override
int get selectedRowCount => 0;
Expand Down
5 changes: 4 additions & 1 deletion lib/content_management/view/create_category_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -82,7 +83,9 @@ class _CreateCategoryViewState extends State<_CreateCategoryView> {
),
);
context.read<ContentManagementBloc>().add(
const LoadCategoriesRequested(),
const LoadCategoriesRequested(
limit: kDefaultRowsPerPage,
),
);
context.pop();
}
Expand Down
5 changes: 4 additions & 1 deletion lib/content_management/view/create_headline_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -84,7 +85,9 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> {
),
);
context.read<ContentManagementBloc>().add(
const LoadHeadlinesRequested(),
const LoadHeadlinesRequested(
limit: kDefaultRowsPerPage,
),
);
context.pop();
}
Expand Down
5 changes: 4 additions & 1 deletion lib/content_management/view/create_source_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -82,7 +83,9 @@ class _CreateSourceViewState extends State<_CreateSourceView> {
SnackBar(content: Text(l10n.sourceCreatedSuccessfully)),
);
context.read<ContentManagementBloc>().add(
const LoadSourcesRequested(),
const LoadSourcesRequested(
limit: kDefaultRowsPerPage,
),
);
context.pop();
}
Expand Down
5 changes: 4 additions & 1 deletion lib/content_management/view/edit_category_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -108,7 +109,9 @@ class _EditCategoryViewState extends State<_EditCategoryView> {
const SnackBar(content: Text('Category updated successfully.')),
);
context.read<ContentManagementBloc>().add(
const LoadCategoriesRequested(),
const LoadCategoriesRequested(
limit: kDefaultRowsPerPage,
),
);
context.pop();
}
Expand Down
5 changes: 4 additions & 1 deletion lib/content_management/view/edit_headline_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -114,7 +115,9 @@ class _EditHeadlineViewState extends State<_EditHeadlineView> {
),
);
context.read<ContentManagementBloc>().add(
const LoadHeadlinesRequested(),
const LoadHeadlinesRequested(
limit: kDefaultRowsPerPage,
),
);
context.pop();
}
Expand Down
Loading
Loading