Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6e62296
feat(l10n): add localization for user promotion and demotion dialogs
fulleni Dec 13, 2025
b9c5e78
build(l10n): sync
fulleni Dec 13, 2025
bd9deb2
feat(ui): add AppUserRoleUI extension for premium user helpers
fulleni Dec 13, 2025
89efeb9
feat(shared): add ConfirmationDialog widget
fulleni Dec 13, 2025
0add089
feat(user_management): enhance user filtering functionality
fulleni Dec 13, 2025
9acfce0
refactor(user_management): update UserFilterState for enhanced user f…
fulleni Dec 13, 2025
585bad8
refactor(user_management): update UserFilterEvent properties and remo…
fulleni Dec 13, 2025
788d1dc
refactor(user_management): update user filter logic and enums
fulleni Dec 13, 2025
f3c10c5
feat(user_management): add authentication filter enum and localization
fulleni Dec 13, 2025
4c8a0f0
feat(user_management): add subscription filter enum and localization
fulleni Dec 13, 2025
faeb855
feat(user_management): add UI-related helper for dashboard user roles
fulleni Dec 13, 2025
1cc7917
feat(user_management): add confirmation dialogs for promote and demot…
fulleni Dec 13, 2025
d50c2bb
refactor(user_filter_dialog): update state to use filter enums
fulleni Dec 13, 2025
288e6db
refactor(user_management): update user filter dialog events
fulleni Dec 13, 2025
30ae36f
refactor(user_management): update UserFilterDialogBloc with new filters
fulleni Dec 13, 2025
855d14b
refactor(user_management): revamp user filter dialog
fulleni Dec 13, 2025
8c2d001
chore: barrels
fulleni Dec 13, 2025
84a4143
feat(l10n): add tooltips for premium and privileged users
fulleni Dec 13, 2025
2f8d681
build(l10n): sync.
fulleni Dec 13, 2025
09122de
refactor(user_management): replace indicator dots with tooltip icons
fulleni Dec 13, 2025
20c84ca
feat(l10n): improve user role tooltips and translations
fulleni Dec 13, 2025
18ec2c1
build(l10n): sync
fulleni Dec 13, 2025
6385ec4
feat(user_management): add dashboard role filter functionality
fulleni Dec 13, 2025
3f8f9df
fix(user-management): wrap user email in SingleChildScrollView for sc…
fulleni Dec 13, 2025
2f01319
fix(user_management): remove redundant role filter check
fulleni Dec 13, 2025
ca35dd9
fix(user_management): update user filter dialog
fulleni Dec 13, 2025
81ff7a2
refactor(user-management): improve code readability in UserFilterDial…
fulleni Dec 13, 2025
9028e5a
fix(user-management): adjust role selection logic in user filter dialog
fulleni Dec 13, 2025
df5f1e9
feat(ui): wrap premium icon with tooltip for screen readers
fulleni Dec 13, 2025
6153f23
refactor(user_management): remove unused buildFilterMap function
fulleni Dec 13, 2025
a60e33b
refactor(users): improve email display and role icon logic
fulleni Dec 13, 2025
6efae19
style: format
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
42 changes: 42 additions & 0 deletions lib/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3997,6 +3997,48 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Community'**
String get navCommunity;

/// Title for the dialog confirming user promotion.
///
/// In en, this message translates to:
/// **'Confirm Promotion'**
String get confirmPromotionTitle;

/// Message for the dialog confirming user promotion.
///
/// In en, this message translates to:
/// **'Are you sure you want to promote {email} to a Publisher?'**
String confirmPromotionMessage(String email);

/// Title for the dialog confirming user demotion.
///
/// In en, this message translates to:
/// **'Confirm Demotion'**
String get confirmDemotionTitle;

/// Message for the dialog confirming user demotion.
///
/// In en, this message translates to:
/// **'Are you sure you want to demote {email} to a standard user?'**
String confirmDemotionMessage(String email);

/// Tooltip for the icon indicating a premium user.
///
/// In en, this message translates to:
/// **'Premium'**
String get premiumUserTooltip;

/// Tooltip for the icon indicating an admin user.
///
/// In en, this message translates to:
/// **'Admin'**
String get adminUserTooltip;

/// Tooltip for the icon indicating a publisher user.
///
/// In en, this message translates to:
/// **'Publisher'**
String get publisherUserTooltip;
}

class _AppLocalizationsDelegate
Expand Down
25 changes: 25 additions & 0 deletions lib/l10n/app_localizations_ar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2142,4 +2142,29 @@ class AppLocalizationsAr extends AppLocalizations {

@override
String get navCommunity => 'المجتمع';

@override
String get confirmPromotionTitle => 'تأكيد الترقية';

@override
String confirmPromotionMessage(String email) {
return 'هل أنت متأكد أنك تريد ترقية $email إلى ناشر؟';
}

@override
String get confirmDemotionTitle => 'تأكيد التخفيض';

@override
String confirmDemotionMessage(String email) {
return 'هل أنت متأكد أنك تريد تخفيض رتبة $email إلى مستخدم عادي؟';
}

@override
String get premiumUserTooltip => 'مستخدم مميز';

@override
String get adminUserTooltip => 'مسؤول';

@override
String get publisherUserTooltip => 'ناشر';
}
25 changes: 25 additions & 0 deletions lib/l10n/app_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2148,4 +2148,29 @@ class AppLocalizationsEn extends AppLocalizations {

@override
String get navCommunity => 'Community';

@override
String get confirmPromotionTitle => 'Confirm Promotion';

@override
String confirmPromotionMessage(String email) {
return 'Are you sure you want to promote $email to a Publisher?';
}

@override
String get confirmDemotionTitle => 'Confirm Demotion';

@override
String confirmDemotionMessage(String email) {
return 'Are you sure you want to demote $email to a standard user?';
}

@override
String get premiumUserTooltip => 'Premium';

@override
String get adminUserTooltip => 'Admin';

@override
String get publisherUserTooltip => 'Publisher';
}
38 changes: 38 additions & 0 deletions lib/l10n/arb/app_ar.arb
Original file line number Diff line number Diff line change
Expand Up @@ -2698,5 +2698,43 @@
"navCommunity": "المجتمع",
"@navCommunity": {
"description": "تسمية تنقل قصيرة لإدارة المجتمع."
},
"confirmPromotionTitle": "تأكيد الترقية",
"@confirmPromotionTitle": {
"description": "عنوان مربع حوار تأكيد ترقية المستخدم."
},
"confirmPromotionMessage": "هل أنت متأكد أنك تريد ترقية {email} إلى ناشر؟",
"@confirmPromotionMessage": {
"description": "رسالة مربع حوار تأكيد ترقية المستخدم.",
"placeholders": {
"email": {
"type": "String"
}
}
},
"confirmDemotionTitle": "تأكيد التخفيض",
"@confirmDemotionTitle": {
"description": "عنوان مربع حوار تأكيد تخفيض رتبة المستخدم."
},
"confirmDemotionMessage": "هل أنت متأكد أنك تريد تخفيض رتبة {email} إلى مستخدم عادي؟",
"@confirmDemotionMessage": {
"description": "رسالة مربع حوار تأكيد تخفيض رتبة المستخدم.",
"placeholders": {
"email": {
"type": "String"
}
}
},
"premiumUserTooltip": "مستخدم مميز",
"@premiumUserTooltip": {
"description": "تلميح للأيقونة التي تشير إلى مستخدم مميز."
},
"adminUserTooltip": "مسؤول",
"@adminUserTooltip": {
"description": "تلميح للأيقونة التي تشير إلى مستخدم مسؤول."
},
"publisherUserTooltip": "ناشر",
"@publisherUserTooltip": {
"description": "تلميح للأيقونة التي تشير إلى مستخدم ناشر."
}
}
38 changes: 38 additions & 0 deletions lib/l10n/arb/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -2694,5 +2694,43 @@
"navCommunity": "Community",
"@navCommunity": {
"description": "Short navigation label for Community Management."
},
"confirmPromotionTitle": "Confirm Promotion",
"@confirmPromotionTitle": {
"description": "Title for the dialog confirming user promotion."
},
"confirmPromotionMessage": "Are you sure you want to promote {email} to a Publisher?",
"@confirmPromotionMessage": {
"description": "Message for the dialog confirming user promotion.",
"placeholders": {
"email": {
"type": "String"
}
}
},
"confirmDemotionTitle": "Confirm Demotion",
"@confirmDemotionTitle": {
"description": "Title for the dialog confirming user demotion."
},
"confirmDemotionMessage": "Are you sure you want to demote {email} to a standard user?",
"@confirmDemotionMessage": {
"description": "Message for the dialog confirming user demotion.",
"placeholders": {
"email": {
"type": "String"
}
}
},
"premiumUserTooltip": "Premium",
"@premiumUserTooltip": {
"description": "Tooltip for the icon indicating a premium user."
},
"adminUserTooltip": "Admin",
"@adminUserTooltip": {
"description": "Tooltip for the icon indicating an admin user."
},
"publisherUserTooltip": "Publisher",
"@publisherUserTooltip": {
"description": "Tooltip for the icon indicating a publisher user."
}
}
23 changes: 23 additions & 0 deletions lib/shared/extensions/app_user_role_ui.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'package:core/core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart';

/// An extension on [AppUserRole] to provide UI-related helpers.
extension AppUserRoleUI on AppUserRole {
/// A convenience getter to check if the user role is premium.
bool get isPremium => this == AppUserRole.premiumUser;

/// Returns a premium indicator icon wrapped in a tooltip if the user is a
/// premium user.
///
/// Returns a gold star icon for premium users, otherwise returns null.
Widget? getPremiumIcon(AppLocalizations l10n) {
if (isPremium) {
return Tooltip(
message: l10n.premiumUserTooltip,
child: const Icon(Icons.star, color: Colors.amber, size: 16),
);
}
return null;
}
}
55 changes: 55 additions & 0 deletions lib/shared/widgets/confirmation_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart';

/// {@template confirmation_dialog}
/// A reusable dialog to confirm a user action.
///
/// This dialog displays a title, content, and two buttons: a cancel button
/// and a confirm button. The text for these buttons and the action to be
/// performed on confirmation are customizable.
/// {@endtemplate}
class ConfirmationDialog extends StatelessWidget {
/// {@macro confirmation_dialog}
const ConfirmationDialog({
required this.title,
required this.content,
required this.onConfirm,
this.confirmText,
super.key,
});

/// The title of the dialog.
final String title;

/// The main content or question of the dialog.
final String content;

/// The callback to be executed when the user confirms the action.
final VoidCallback onConfirm;

/// The text for the confirmation button. Defaults to 'Confirm'.
final String? confirmText;

@override
Widget build(BuildContext context) {
final l10n = AppLocalizationsX(context).l10n;

return AlertDialog(
title: Text(title),
content: Text(content),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(l10n.cancelButton),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
onConfirm();
},
child: Text(confirmText ?? l10n.confirmSaveButton),
),
],
);
}
}
2 changes: 2 additions & 0 deletions lib/shared/widgets/selection_page/selection_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export 'searchable_selection_page.dart';
export 'selection_page_arguments.dart';
6 changes: 4 additions & 2 deletions lib/shared/widgets/widgets.dart
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export 'package:flutter_news_app_web_dashboard_full_source_code/shared/widgets/about_icon.dart';
export 'package:flutter_news_app_web_dashboard_full_source_code/shared/widgets/searchable_selection_input.dart';
export 'about_icon.dart';
export 'confirmation_dialog.dart';
export 'searchable_selection_input.dart';
export 'selection_page/selection_page.dart';
51 changes: 5 additions & 46 deletions lib/user_management/bloc/user_filter/user_filter_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import 'package:core/core.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_news_app_web_dashboard_full_source_code/user_management/bloc/user_management_bloc.dart'
show UserManagementBloc;
import 'package:flutter_news_app_web_dashboard_full_source_code/user_management/enums/authentication_filter.dart';
import 'package:flutter_news_app_web_dashboard_full_source_code/user_management/enums/subscription_filter.dart';

part 'user_filter_event.dart';
part 'user_filter_state.dart';
Expand All @@ -18,8 +20,6 @@ class UserFilterBloc extends Bloc<UserFilterEvent, UserFilterState> {
/// {@macro user_filter_bloc}
UserFilterBloc() : super(const UserFilterState()) {
on<UserFilterSearchQueryChanged>(_onSearchQueryChanged);
on<UserFilterAppRolesChanged>(_onAppRolesChanged);
on<UserFilterDashboardRolesChanged>(_onDashboardRolesChanged);
on<UserFilterReset>(_onFilterReset);
on<UserFilterApplied>(_onFilterApplied);
}
Expand All @@ -32,22 +32,6 @@ class UserFilterBloc extends Bloc<UserFilterEvent, UserFilterState> {
emit(state.copyWith(searchQuery: event.query));
}

/// Handles changes to the selected app roles filter.
void _onAppRolesChanged(
UserFilterAppRolesChanged event,
Emitter<UserFilterState> emit,
) {
emit(state.copyWith(selectedAppRoles: event.appRoles));
}

/// Handles changes to the selected dashboard roles filter.
void _onDashboardRolesChanged(
UserFilterDashboardRolesChanged event,
Emitter<UserFilterState> emit,
) {
emit(state.copyWith(selectedDashboardRoles: event.dashboardRoles));
}

/// Resets all filters to their default values.
void _onFilterReset(
UserFilterReset event,
Expand All @@ -64,35 +48,10 @@ class UserFilterBloc extends Bloc<UserFilterEvent, UserFilterState> {
emit(
state.copyWith(
searchQuery: event.searchQuery,
selectedAppRoles: event.selectedAppRoles,
selectedDashboardRoles: event.selectedDashboardRoles,
authenticationFilter: event.authenticationFilter,
subscriptionFilter: event.subscriptionFilter,
dashboardRole: event.dashboardRole,
),
);
}

/// Builds the filter map for the data repository query.
Map<String, dynamic> buildFilterMap() {
final filter = <String, dynamic>{};

if (state.searchQuery.isNotEmpty) {
filter[r'$or'] = [
{
'email': {r'$regex': state.searchQuery, r'$options': 'i'},
},
{'_id': state.searchQuery},
];
}

if (state.selectedAppRoles.isNotEmpty) {
filter['appRole'] = {
r'$in': state.selectedAppRoles.map((r) => r.name).toList(),
};
}
if (state.selectedDashboardRoles.isNotEmpty) {
filter['dashboardRole'] = {
r'$in': state.selectedDashboardRoles.map((r) => r.name).toList(),
};
}
return filter;
}
}
Loading
Loading