Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
045f356
feat(l10n): add tooltip translations for breaking news icon
fulleni Dec 13, 2025
3700515
build(l10n): sync
fulleni Dec 13, 2025
c69d9f0
fix(content_management): improve breaking news display and accessibility
fulleni Dec 13, 2025
1201b0b
feat(l10n): add translations for breaking news filter description
fulleni Dec 13, 2025
27a47ff
build(l10n): sync
fulleni Dec 13, 2025
8b35034
refactor(content_management): simplify breaking news filter flag
fulleni Dec 13, 2025
09a2c46
refactor(content_management): update breaking news filter handling
fulleni Dec 13, 2025
4397dbb
refactor(content): simplify breaking news filter logic
fulleni Dec 13, 2025
75e97d2
refactor(content_management): simplify breaking news filter logic
fulleni Dec 13, 2025
3aba00d
fix(filter-dialog): change type of isBreaking to bool
fulleni Dec 13, 2025
1f1aeca
fix(content_management): update filter dialog reset functionality
fulleni Dec 13, 2025
8f1b727
refactor(content_management): simplify headlines filter logic
fulleni Dec 13, 2025
6002bd2
refactor(filter_dialog): replace BreakingNewsFilterStatus with boolean
fulleni Dec 13, 2025
83ac65d
feat(l10n): add confirmation dialog translations for item actions
fulleni Dec 13, 2025
069259f
build(l10n): sync
fulleni Dec 13, 2025
edabb9c
feat(content_management): add confirmation dialog for content actions
fulleni Dec 13, 2025
4c18673
feat(localization): add Arabic translations and update deleted item s…
fulleni Dec 13, 2025
e098322
build(l10n): sync.
fulleni Dec 13, 2025
69b04d5
refactor(content_management): replace snackbarMessage with itemPendin…
fulleni Dec 13, 2025
6f3c949
feat(content_management): improve snackbar behavior and state management
fulleni Dec 13, 2025
7c8ae99
feat(content_management): improve snackbar message for item deletion
fulleni Dec 13, 2025
2befa95
feat(pending_deletions_service): add messageBuilder to requestDeletion
fulleni Dec 13, 2025
2fe7cf0
refactor(content_management): add case for DeletionStatus.requested i…
fulleni Dec 13, 2025
6c3c35c
feat(pending_deletions_service): add requested state and event for pe…
fulleni Dec 13, 2025
2e3fbd6
feat(l10n): add Arabic translations for new labels
fulleni Dec 13, 2025
aa6ef2c
build(l10n): sync.
fulleni Dec 13, 2025
96ff3e5
fix(content_management): correct item type naming in content management
fulleni Dec 13, 2025
a3fd5d3
fix(l10n): improve confirmation dialog messages for item actions
fulleni Dec 13, 2025
50ac9b5
build(l10n): sync
fulleni Dec 13, 2025
d21bebe
fix(content): update confirmation dialog messages for different conte…
fulleni Dec 13, 2025
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
45 changes: 24 additions & 21 deletions lib/content_management/bloc/content_management_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import 'package:equatable/equatable.dart';
import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/headlines_filter/headlines_filter_bloc.dart';
import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/sources_filter/sources_filter_bloc.dart';
import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/bloc/topics_filter/topics_filter_bloc.dart';
import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/models/breaking_news_filter_status.dart';
import 'package:flutter_news_app_web_dashboard_full_source_code/shared/constants/app_constants.dart';
import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/pending_deletions_service.dart';
import 'package:ui_kit/ui_kit.dart';
Expand Down Expand Up @@ -154,15 +153,9 @@ class ContentManagementBloc
filter['eventCountry.id'] = {r'$in': state.selectedCountryIds};
}

// Handle the breaking news filter based on the enum status.
switch (state.isBreaking) {
case BreakingNewsFilterStatus.breakingOnly:
filter['isBreaking'] = true;
case BreakingNewsFilterStatus.nonBreakingOnly:
filter['isBreaking'] = false;
case BreakingNewsFilterStatus.all:
// For 'all', we don't add the 'isBreaking' key to the filter.
break;
// If the breaking news filter is active, add it to the query.
if (state.isBreaking) {
filter['isBreaking'] = true;
}

return filter;
Expand Down Expand Up @@ -394,14 +387,15 @@ class ContentManagementBloc
state.copyWith(
headlines: updatedHeadlines,
lastPendingDeletionId: event.id,
snackbarMessage: 'Headline "${headlineToDelete.title}" deleted.',
itemPendingDeletion: headlineToDelete,
),
);

_pendingDeletionsService.requestDeletion(
item: headlineToDelete,
repository: _headlinesRepository,
undoDuration: AppConstants.kSnackbarDuration,
// messageBuilder is omitted, UI will build the message
);
}

Expand Down Expand Up @@ -585,14 +579,15 @@ class ContentManagementBloc
state.copyWith(
topics: updatedTopics,
lastPendingDeletionId: event.id,
snackbarMessage: 'Topic "${topicToDelete.name}" deleted.',
itemPendingDeletion: topicToDelete,
),
);

_pendingDeletionsService.requestDeletion(
item: topicToDelete,
repository: _topicsRepository,
undoDuration: AppConstants.kSnackbarDuration,
// messageBuilder is omitted, UI will build the message
);
}

Expand Down Expand Up @@ -776,14 +771,15 @@ class ContentManagementBloc
state.copyWith(
sources: updatedSources,
lastPendingDeletionId: event.id,
snackbarMessage: 'Source "${sourceToDelete.name}" deleted.',
itemPendingDeletion: sourceToDelete,
),
);

_pendingDeletionsService.requestDeletion(
item: sourceToDelete,
repository: _sourcesRepository,
undoDuration: AppConstants.kSnackbarDuration,
// messageBuilder is omitted, UI will build the message
);
}

Expand All @@ -804,13 +800,20 @@ class ContentManagementBloc
Emitter<ContentManagementState> emit,
) async {
switch (event.event.status) {
case DeletionStatus.requested:
// This case is now handled by the optimistic UI update in the
// specific delete handlers (e.g., _onDeleteHeadlineForeverRequested).
// The itemPendingDeletion is set there, which the UI uses to build
// the snackbar message.
break;
case DeletionStatus.confirmed:
// If deletion is confirmed, clear pending status.
// The item was already optimistically removed from the list.
emit(
state.copyWith(
lastPendingDeletionId: null,
snackbarMessage: null,
lastPendingDeletionId: null, // Clear the pending ID
// Clear the item so the snackbar doesn't reappear on rebuilds
itemPendingDeletion: null,
),
);
case DeletionStatus.undone:
Expand All @@ -823,8 +826,8 @@ class ContentManagementBloc
emit(
state.copyWith(
headlines: updatedHeadlines,
lastPendingDeletionId: null,
snackbarMessage: null,
lastPendingDeletionId: null, // Clear the pending ID
itemPendingDeletion: null,
),
);
} else if (item is Topic) {
Expand All @@ -834,8 +837,8 @@ class ContentManagementBloc
emit(
state.copyWith(
topics: updatedTopics,
lastPendingDeletionId: null,
snackbarMessage: null,
lastPendingDeletionId: null, // Clear the pending ID
itemPendingDeletion: null,
),
);
} else if (item is Source) {
Expand All @@ -845,8 +848,8 @@ class ContentManagementBloc
emit(
state.copyWith(
sources: updatedSources,
lastPendingDeletionId: null,
snackbarMessage: null,
lastPendingDeletionId: null, // Clear the pending ID
itemPendingDeletion: null,
),
);
}
Expand Down
14 changes: 7 additions & 7 deletions lib/content_management/bloc/content_management_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class ContentManagementState extends Equatable {
this.sourcesHasMore = false,
this.exception,
this.lastPendingDeletionId,
this.snackbarMessage,
this.itemPendingDeletion,
});

/// The currently active tab in the content management section.
Expand Down Expand Up @@ -85,9 +85,9 @@ class ContentManagementState extends Equatable {
/// Used to trigger the snackbar display.
final String? lastPendingDeletionId;

/// The message to display in the snackbar for pending deletions or other
/// transient messages.
final String? snackbarMessage;
/// The item that was just requested for deletion, used by the UI to show
/// a confirmation snackbar.
final FeedItem? itemPendingDeletion;

/// Creates a copy of this [ContentManagementState] with updated values.
ContentManagementState copyWith({
Expand All @@ -106,7 +106,7 @@ class ContentManagementState extends Equatable {
bool? sourcesHasMore,
HttpException? exception,
String? lastPendingDeletionId,
String? snackbarMessage,
FeedItem? itemPendingDeletion,
}) {
return ContentManagementState(
activeTab: activeTab ?? this.activeTab,
Expand All @@ -124,7 +124,7 @@ class ContentManagementState extends Equatable {
sourcesHasMore: sourcesHasMore ?? this.sourcesHasMore,
exception: exception,
lastPendingDeletionId: lastPendingDeletionId,
snackbarMessage: snackbarMessage,
itemPendingDeletion: itemPendingDeletion,
);
}

Expand All @@ -145,6 +145,6 @@ class ContentManagementState extends Equatable {
sourcesHasMore,
exception,
lastPendingDeletionId,
snackbarMessage,
itemPendingDeletion,
];
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import 'package:bloc/bloc.dart';
import 'package:core/core.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_news_app_web_dashboard_full_source_code/content_management/models/breaking_news_filter_status.dart';

part 'headlines_filter_event.dart';
part 'headlines_filter_state.dart';
Expand Down Expand Up @@ -79,8 +78,7 @@ class HeadlinesFilterBloc

/// Handles changes to the breaking news filter.
///
/// This updates the `isBreaking` status for the filter using the
/// [BreakingNewsFilterStatus] enum.
/// This updates the `isBreaking` status for the filter.
void _onHeadlinesBreakingNewsFilterChanged(
HeadlinesBreakingNewsFilterChanged event,
Emitter<HeadlinesFilterState> emit,
Expand Down Expand Up @@ -139,9 +137,8 @@ class HeadlinesFilterBloc
if (state.selectedCountryIds.isNotEmpty) {
filter['eventCountry.id'] = {r'$in': state.selectedCountryIds};
}
if (state.isBreaking != BreakingNewsFilterStatus.all) {
filter['isBreaking'] =
state.isBreaking == BreakingNewsFilterStatus.breakingOnly;
if (state.isBreaking) {
filter['isBreaking'] = true;
}

return filter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,10 @@ final class HeadlinesCountryFilterChanged extends HeadlinesFilterEvent {
final class HeadlinesBreakingNewsFilterChanged extends HeadlinesFilterEvent {
const HeadlinesBreakingNewsFilterChanged(this.isBreaking);

final BreakingNewsFilterStatus isBreaking;
final bool isBreaking;

@override
List<Object?> get props => [isBreaking];
List<Object> get props => [isBreaking];
}

/// Event to request applying all current filters.
Expand All @@ -84,7 +84,7 @@ final class HeadlinesFilterApplied extends HeadlinesFilterEvent {
final List<String> selectedSourceIds;
final List<String> selectedTopicIds;
final List<String> selectedCountryIds;
final BreakingNewsFilterStatus isBreaking;
final bool isBreaking;

@override
List<Object?> get props => [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class HeadlinesFilterState extends Equatable {
this.selectedSourceIds = const [],
this.selectedTopicIds = const [],
this.selectedCountryIds = const [],
this.isBreaking = BreakingNewsFilterStatus.all,
this.isBreaking = false,
});

/// The current text in the search query field.
Expand All @@ -33,9 +33,8 @@ class HeadlinesFilterState extends Equatable {
/// The list of country IDs to be included in the filter.
final List<String> selectedCountryIds;

/// The breaking news status to filter by.
/// `null` = all, `true` = breaking only, `false` = non-breaking only.
final BreakingNewsFilterStatus isBreaking;
/// A flag to filter for breaking news only.
final bool isBreaking;

/// Creates a copy of this state with the given fields replaced with the
/// new values.
Expand All @@ -45,7 +44,7 @@ class HeadlinesFilterState extends Equatable {
List<String>? selectedSourceIds,
List<String>? selectedTopicIds,
List<String>? selectedCountryIds,
BreakingNewsFilterStatus? isBreaking,
bool? isBreaking,
}) {
return HeadlinesFilterState(
searchQuery: searchQuery ?? this.searchQuery,
Expand Down
19 changes: 16 additions & 3 deletions lib/content_management/view/content_management_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,27 @@ class _ContentManagementPageState extends State<ContentManagementPage>
),
BlocListener<ContentManagementBloc, ContentManagementState>(
listenWhen: (previous, current) =>
previous.snackbarMessage != current.snackbarMessage &&
current.snackbarMessage != null,
previous.itemPendingDeletion != current.itemPendingDeletion &&
current.itemPendingDeletion != null,
listener: (context, state) {
final item = state.itemPendingDeletion!;
String itemType;
String itemName;
if (item is Headline) {
itemType = l10n.headline;
itemName = item.title;
} else if (item is Topic) {
itemType = l10n.topic;
itemName = item.name;
} else {
itemType = l10n.source;
itemName = (item as Source).name;
}
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
content: Text(state.snackbarMessage!),
content: Text(l10n.itemDeletedSnackbar(itemType, itemName)),
action: SnackBarAction(
label: l10n.undo,
onPressed: () {
Expand Down
50 changes: 24 additions & 26 deletions lib/content_management/view/headlines_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -221,32 +221,30 @@ class _HeadlinesDataSource extends DataTableSource {
},
cells: [
DataCell(
Stack(
alignment: AlignmentDirectional.centerStart,
children: <Widget>[
// Add padding to the text to make space for the icon only when
// the headline is breaking news. This ensures all titles align
// vertically regardless of the icon's presence.
Padding(
padding: EdgeInsetsDirectional.only(
start: headline.isBreaking
? AppSpacing.xl + AppSpacing.xs
: 0,
),
child: Text(
headline.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
// Conditionally display the icon at the start of the cell.
if (headline.isBreaking)
Icon(
Icons.flash_on,
size: 18,
color: Theme.of(context).colorScheme.primary,
),
],
RichText(
maxLines: 2,
overflow: TextOverflow.ellipsis,
text: TextSpan(
style: Theme.of(context).textTheme.bodyMedium,
children: [
TextSpan(text: headline.title),
if (headline.isBreaking)
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Padding(
padding: const EdgeInsets.only(left: AppSpacing.xs),
child: Tooltip(
message: l10n.breakingNewsHint,
child: Icon(
Icons.flash_on,
size: 14,
color: Theme.of(context).colorScheme.primary,
),
),
),
),
],
),
),
),
if (!isMobile) // Conditionally show Source Name
Expand Down
Loading
Loading