From 6e622965757d2635ff626cd431058641c47153a4 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 05:34:33 +0100 Subject: [PATCH 01/32] feat(l10n): add localization for user promotion and demotion dialogs - Add Arabic and English translations for confirm promotion and demotion titles and messages - Include placeholders for user email in promotion and demotion messages --- lib/l10n/arb/app_ar.arb | 26 ++++++++++++++++++++++++++ lib/l10n/arb/app_en.arb | 26 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index f6dfd1d9..d499bed8 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -2698,5 +2698,31 @@ "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" + } + } } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index e0dce8e8..0a514192 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2694,5 +2694,31 @@ "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" + } + } } } \ No newline at end of file From b9c5e7811cc3b6f87631c3659380f847052411d2 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 05:34:50 +0100 Subject: [PATCH 02/32] build(l10n): sync --- lib/l10n/app_localizations.dart | 24 ++++++++++++++++++++++++ lib/l10n/app_localizations_ar.dart | 16 ++++++++++++++++ lib/l10n/app_localizations_en.dart | 16 ++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 7613024d..f0f7183e 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3997,6 +3997,30 @@ 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); } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 7d47caca..81f0e38e 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -2142,4 +2142,20 @@ 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 إلى مستخدم عادي؟'; + } } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 0c5af7b8..20c92458 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2148,4 +2148,20 @@ 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?'; + } } From bd9deb273c84f6c91dfd04aa187e14fea53384f6 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 05:36:39 +0100 Subject: [PATCH 03/32] feat(ui): add AppUserRoleUI extension for premium user helpers - Create new extension on AppUserRole for UI-related functionalities - Add isPremium getter to check if user role is premium - Implement premiumIcon getter to return a gold star icon for premium users --- lib/shared/extensions/app_user_role_ui.dart | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 lib/shared/extensions/app_user_role_ui.dart diff --git a/lib/shared/extensions/app_user_role_ui.dart b/lib/shared/extensions/app_user_role_ui.dart new file mode 100644 index 00000000..c3e57a70 --- /dev/null +++ b/lib/shared/extensions/app_user_role_ui.dart @@ -0,0 +1,21 @@ +import 'package:core/core.dart'; +import 'package:flutter/material.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 if the user is a premium user. + /// + /// Returns a gold star icon for premium users, otherwise returns null. + Widget? get premiumIcon { + if (isPremium) { + return const Padding( + padding: EdgeInsets.only(left: 8), + child: Icon(Icons.star, color: Colors.amber, size: 16), + ); + } + return null; + } +} From 89efeb9363e4615ef9ec594b4edc2c6f5040fa1d Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 05:37:15 +0100 Subject: [PATCH 04/32] feat(shared): add ConfirmationDialog widget - Create a reusable confirmation dialog widget for user actions - Include customizable title, content, and confirmation button text - Support for localization through AppLocalizationsX - Provide callback functionality for confirmed actions --- lib/shared/widgets/confirmation_dialog.dart | 55 +++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 lib/shared/widgets/confirmation_dialog.dart diff --git a/lib/shared/widgets/confirmation_dialog.dart b/lib/shared/widgets/confirmation_dialog.dart new file mode 100644 index 00000000..6c4047c6 --- /dev/null +++ b/lib/shared/widgets/confirmation_dialog.dart @@ -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: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(l10n.cancelButton), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + onConfirm(); + }, + child: Text(confirmText ?? l10n.confirmSaveButton), + ), + ], + ); + } +} From 0add089b365eeab97743b98e0480d5761c9ab54c Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 05:38:47 +0100 Subject: [PATCH 05/32] feat(user_management): enhance user filtering functionality - Add support for advanced filtering by authentication and subscription status - Implement logical OR conditions for search queries - Refactor user filter map building process for better readability and maintainability - Introduce new enums for authentication and subscription filtering --- .../bloc/user_management_bloc.dart | 54 +++++++++++++++---- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/lib/user_management/bloc/user_management_bloc.dart b/lib/user_management/bloc/user_management_bloc.dart index 9a50b77d..955d2650 100644 --- a/lib/user_management/bloc/user_management_bloc.dart +++ b/lib/user_management/bloc/user_management_bloc.dart @@ -6,6 +6,8 @@ import 'package:data_repository/data_repository.dart'; import 'package:equatable/equatable.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/user_management/bloc/user_filter/user_filter_bloc.dart'; +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'; import 'package:logging/logging.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -77,20 +79,54 @@ class UserManagementBloc Map buildUsersFilterMap(UserFilterState state) { final filter = {}; + final orConditions = >[]; + if (state.searchQuery.isNotEmpty) { - filter['email'] = {r'$regex': state.searchQuery, r'$options': 'i'}; + orConditions + ..add({ + 'email': {r'$regex': state.searchQuery, r'$options': 'i'}, + }) + ..add({'_id': state.searchQuery}); + } + + if (orConditions.isNotEmpty) { + filter[r'$or'] = orConditions; } - if (state.selectedAppRoles.isNotEmpty) { - filter['appRole'] = { - r'$in': state.selectedAppRoles.map((r) => r.name).toList(), - }; + final authRoles = {}; + + switch (state.authenticationFilter) { + case AuthenticationFilter.authenticated: + authRoles.addAll([ + AppUserRole.standardUser.name, + AppUserRole.premiumUser.name, + ]); + case AuthenticationFilter.anonymous: + authRoles.add(AppUserRole.guestUser.name); + case AuthenticationFilter.all: + break; } - if (state.selectedDashboardRoles.isNotEmpty) { - filter['dashboardRole'] = { - r'$in': state.selectedDashboardRoles.map((r) => r.name).toList(), - }; + final subscriptionRoles = {}; + switch (state.subscriptionFilter) { + case SubscriptionFilter.premium: + subscriptionRoles.add(AppUserRole.premiumUser.name); + case SubscriptionFilter.free: + subscriptionRoles.addAll([ + AppUserRole.guestUser.name, + AppUserRole.standardUser.name, + ]); + case SubscriptionFilter.all: + break; + } + + final intersectingRoles = + authRoles.isNotEmpty && subscriptionRoles.isNotEmpty + ? authRoles.intersection(subscriptionRoles) + : (authRoles.isNotEmpty ? authRoles : subscriptionRoles); + + if (intersectingRoles.isNotEmpty) { + filter['appRole'] = {r'$in': intersectingRoles.toList()}; } return filter; From 9acfce072f94a0361535c7ec39b1711ce9ce3427 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 05:39:03 +0100 Subject: [PATCH 06/32] refactor(user_management): update UserFilterState for enhanced user filtering - Replace selectedAppRoles and selectedDashboardRoles with individual filters - Add AuthenticationFilter, SubscriptionFilter, and DashboardUserRole? for more granular filtering - Update copyWith method to accommodate new filter structure - Modify props to include new filter properties --- .../bloc/user_filter/user_filter_state.dart | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/lib/user_management/bloc/user_filter/user_filter_state.dart b/lib/user_management/bloc/user_filter/user_filter_state.dart index 6381b3de..794cac0a 100644 --- a/lib/user_management/bloc/user_filter/user_filter_state.dart +++ b/lib/user_management/bloc/user_filter/user_filter_state.dart @@ -10,37 +10,43 @@ class UserFilterState extends Equatable { /// {@macro user_filter_state} const UserFilterState({ this.searchQuery = '', - this.selectedAppRoles = const [], - this.selectedDashboardRoles = const [], + this.authenticationFilter = AuthenticationFilter.all, + this.subscriptionFilter = SubscriptionFilter.all, + this.dashboardRole, }); /// The current search query for filtering users by email. final String searchQuery; - /// The list of selected app roles to filter users by. - final List selectedAppRoles; + /// The selected authentication status filter. + final AuthenticationFilter authenticationFilter; - /// The list of selected dashboard roles to filter users by. - final List selectedDashboardRoles; + /// The selected subscription status filter. + final SubscriptionFilter subscriptionFilter; + + /// The selected dashboard role filter. + final DashboardUserRole? dashboardRole; /// Creates a copy of this [UserFilterState] with updated values. UserFilterState copyWith({ String? searchQuery, - List? selectedAppRoles, - List? selectedDashboardRoles, + AuthenticationFilter? authenticationFilter, + SubscriptionFilter? subscriptionFilter, + DashboardUserRole? dashboardRole, }) { return UserFilterState( searchQuery: searchQuery ?? this.searchQuery, - selectedAppRoles: selectedAppRoles ?? this.selectedAppRoles, - selectedDashboardRoles: - selectedDashboardRoles ?? this.selectedDashboardRoles, + authenticationFilter: authenticationFilter ?? this.authenticationFilter, + subscriptionFilter: subscriptionFilter ?? this.subscriptionFilter, + dashboardRole: dashboardRole ?? this.dashboardRole, ); } @override List get props => [ searchQuery, - selectedAppRoles, - selectedDashboardRoles, + authenticationFilter, + subscriptionFilter, + dashboardRole ?? '', ]; } From 585bad86a939084a08256a52b7c976403face07c Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 05:41:41 +0100 Subject: [PATCH 07/32] refactor(user_management): update UserFilterEvent properties and remove unused events - Remove UserFilterAppRolesChanged and UserFilterDashboardRolesChanged events - Update UserFilterApplied event to use authenticationFilter, subscriptionFilter, and dashboardRole instead of selectedAppRoles and selectedDashboardRoles - Update corresponding props in UserFilterApplied event --- .../bloc/user_filter/user_filter_event.dart | 35 +++++-------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/lib/user_management/bloc/user_filter/user_filter_event.dart b/lib/user_management/bloc/user_filter/user_filter_event.dart index fa8e3123..7c0aeade 100644 --- a/lib/user_management/bloc/user_filter/user_filter_event.dart +++ b/lib/user_management/bloc/user_filter/user_filter_event.dart @@ -18,26 +18,6 @@ final class UserFilterSearchQueryChanged extends UserFilterEvent { List get props => [query]; } -/// Event to update the selected app roles for filtering users. -final class UserFilterAppRolesChanged extends UserFilterEvent { - const UserFilterAppRolesChanged(this.appRoles); - - final List appRoles; - - @override - List get props => [appRoles]; -} - -/// Event to update the selected dashboard roles for filtering users. -final class UserFilterDashboardRolesChanged extends UserFilterEvent { - const UserFilterDashboardRolesChanged(this.dashboardRoles); - - final List dashboardRoles; - - @override - List get props => [dashboardRoles]; -} - /// Event to reset all filters to their default state. final class UserFilterReset extends UserFilterEvent { const UserFilterReset(); @@ -48,18 +28,21 @@ final class UserFilterReset extends UserFilterEvent { final class UserFilterApplied extends UserFilterEvent { const UserFilterApplied({ required this.searchQuery, - required this.selectedAppRoles, - required this.selectedDashboardRoles, + required this.authenticationFilter, + required this.subscriptionFilter, + this.dashboardRole, }); final String searchQuery; - final List selectedAppRoles; - final List selectedDashboardRoles; + final AuthenticationFilter authenticationFilter; + final SubscriptionFilter subscriptionFilter; + final DashboardUserRole? dashboardRole; @override List get props => [ searchQuery, - selectedAppRoles, - selectedDashboardRoles, + authenticationFilter, + subscriptionFilter, + dashboardRole ?? '', ]; } From 788d1dc59ee23e1b30f82929039324b96f95e85c Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 05:41:56 +0100 Subject: [PATCH 08/32] refactor(user_management): update user filter logic and enums - Replace app and dashboard role filters with authentication and subscription filters - Update filter applied logic based on new filter types - Remove unused event handlers for app and dashboard roles - Add new enums for authentication and subscription filters --- .../bloc/user_filter/user_filter_bloc.dart | 59 ++++++++++--------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/lib/user_management/bloc/user_filter/user_filter_bloc.dart b/lib/user_management/bloc/user_filter/user_filter_bloc.dart index 6f1e1ddd..7e483fda 100644 --- a/lib/user_management/bloc/user_filter/user_filter_bloc.dart +++ b/lib/user_management/bloc/user_filter/user_filter_bloc.dart @@ -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'; @@ -18,8 +20,6 @@ class UserFilterBloc extends Bloc { /// {@macro user_filter_bloc} UserFilterBloc() : super(const UserFilterState()) { on(_onSearchQueryChanged); - on(_onAppRolesChanged); - on(_onDashboardRolesChanged); on(_onFilterReset); on(_onFilterApplied); } @@ -32,22 +32,6 @@ class UserFilterBloc extends Bloc { emit(state.copyWith(searchQuery: event.query)); } - /// Handles changes to the selected app roles filter. - void _onAppRolesChanged( - UserFilterAppRolesChanged event, - Emitter emit, - ) { - emit(state.copyWith(selectedAppRoles: event.appRoles)); - } - - /// Handles changes to the selected dashboard roles filter. - void _onDashboardRolesChanged( - UserFilterDashboardRolesChanged event, - Emitter emit, - ) { - emit(state.copyWith(selectedDashboardRoles: event.dashboardRoles)); - } - /// Resets all filters to their default values. void _onFilterReset( UserFilterReset event, @@ -64,8 +48,9 @@ class UserFilterBloc extends Bloc { emit( state.copyWith( searchQuery: event.searchQuery, - selectedAppRoles: event.selectedAppRoles, - selectedDashboardRoles: event.selectedDashboardRoles, + authenticationFilter: event.authenticationFilter, + subscriptionFilter: event.subscriptionFilter, + dashboardRole: event.dashboardRole, ), ); } @@ -83,16 +68,34 @@ class UserFilterBloc extends Bloc { ]; } - if (state.selectedAppRoles.isNotEmpty) { - filter['appRole'] = { - r'$in': state.selectedAppRoles.map((r) => r.name).toList(), - }; + final appRoleFilter = []; + switch (state.authenticationFilter) { + case AuthenticationFilter.authenticated: + appRoleFilter.addAll( + [AppUserRole.standardUser.name, AppUserRole.premiumUser.name], + ); + case AuthenticationFilter.anonymous: + appRoleFilter.add(AppUserRole.guestUser.name); + case AuthenticationFilter.all: + break; + } + + final subscriptionRoles = {}; + switch (state.subscriptionFilter) { + case SubscriptionFilter.premium: + subscriptionRoles.add(AppUserRole.premiumUser.name); + case SubscriptionFilter.free: + subscriptionRoles.addAll( + [AppUserRole.guestUser.name, AppUserRole.standardUser.name], + ); + case SubscriptionFilter.all: + break; } - if (state.selectedDashboardRoles.isNotEmpty) { - filter['dashboardRole'] = { - r'$in': state.selectedDashboardRoles.map((r) => r.name).toList(), - }; + + if (appRoleFilter.isNotEmpty) { + filter['appRole'] = {r'$in': appRoleFilter.toList()}; } + return filter; } } From f3c10c5ca3d3f90f7b637019f5498189f3796927 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 06:26:08 +0100 Subject: [PATCH 09/32] feat(user_management): add authentication filter enum and localization - Introduce AuthenticationFilter enum to define user authentication status filters - Implement AuthenticationFilterL10n extension for localized string retrieval - Add necessary imports for Flutter and localization support --- .../enums/authentication_filter.dart | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 lib/user_management/enums/authentication_filter.dart diff --git a/lib/user_management/enums/authentication_filter.dart b/lib/user_management/enums/authentication_filter.dart new file mode 100644 index 00000000..57b8c891 --- /dev/null +++ b/lib/user_management/enums/authentication_filter.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; + +/// Defines the filter options for a user's authentication status. +enum AuthenticationFilter { + /// Show all users regardless of authentication status. + all, + + /// Show only authenticated users. + authenticated, + + /// Show only anonymous users. + anonymous, +} + +/// An extension to get the localized string for [AuthenticationFilter]. +extension AuthenticationFilterL10n on AuthenticationFilter { + /// Returns the localized string for the authentication filter option. + String l10n(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + switch (this) { + case AuthenticationFilter.all: + return l10n.any; + case AuthenticationFilter.authenticated: + return l10n.authenticationAuthenticated; + case AuthenticationFilter.anonymous: + return l10n.authenticationAnonymous; + } + } +} From 4c8a0f0a7e10ab6144f9b3434e194521eeea611d Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 06:28:09 +0100 Subject: [PATCH 10/32] feat(user_management): add subscription filter enum and localization - Create SubscriptionFilter enum to define filter options for user's subscription status - Add SubscriptionFilterL10n extension to provide localized strings for subscription filter options - Implement localization support using AppLocalizationsX --- .../enums/subscription_filter.dart | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 lib/user_management/enums/subscription_filter.dart diff --git a/lib/user_management/enums/subscription_filter.dart b/lib/user_management/enums/subscription_filter.dart new file mode 100644 index 00000000..410836cc --- /dev/null +++ b/lib/user_management/enums/subscription_filter.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; + +/// Defines the filter options for a user's subscription status. +enum SubscriptionFilter { + /// Show all users regardless of subscription status. + all, + + /// Show only users with a free subscription. + free, + + /// Show only users with a premium subscription. + premium, +} + +/// An extension to get the localized string for [SubscriptionFilter]. +extension SubscriptionFilterL10n on SubscriptionFilter { + /// Returns the localized string for the subscription filter option. + String l10n(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + switch (this) { + case SubscriptionFilter.all: + return l10n.any; + case SubscriptionFilter.free: + return l10n.subscriptionFree; + case SubscriptionFilter.premium: + return l10n.subscriptionPremium; + } + } +} From faeb8552514fb3745b7517bac6a594caa2ae5824 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 06:28:23 +0100 Subject: [PATCH 11/32] feat(user_management): add UI-related helper for dashboard user roles - Create a new extension on DashboardUserRole to provide UI-related helpers - Add a convenience getter 'isPrivileged' to check if the user has an admin or publisher role --- lib/user_management/view/dashboard_user_role_ui.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 lib/user_management/view/dashboard_user_role_ui.dart diff --git a/lib/user_management/view/dashboard_user_role_ui.dart b/lib/user_management/view/dashboard_user_role_ui.dart new file mode 100644 index 00000000..5ff2fc16 --- /dev/null +++ b/lib/user_management/view/dashboard_user_role_ui.dart @@ -0,0 +1,10 @@ +import 'package:core/core.dart'; + +/// An extension on [DashboardUserRole] to provide UI-related helpers. +extension DashboardUserRoleUI on DashboardUserRole { + /// A convenience getter to check if the user has a privileged dashboard role. + bool get isPrivileged { + return this == DashboardUserRole.admin || + this == DashboardUserRole.publisher; + } +} From 1cc7917e6d5a1073d909471354d277b91faba717 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 06:30:17 +0100 Subject: [PATCH 12/32] feat(user_management): add confirmation dialogs for promote and demote actions - Add ConfirmationDialog widget for promote and demote actions - Implement showDialog for confirmation before changing user role - Update localization keys for new dialog messages --- .../widgets/user_action_buttons.dart | 51 ++++++++++++++----- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/lib/user_management/widgets/user_action_buttons.dart b/lib/user_management/widgets/user_action_buttons.dart index 7ea77570..0d1d0ead 100644 --- a/lib/user_management/widgets/user_action_buttons.dart +++ b/lib/user_management/widgets/user_action_buttons.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart' show ReadContext; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/shared.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/user_management/bloc/user_management_bloc.dart'; /// {@template user_action_buttons} @@ -97,21 +98,43 @@ class UserActionButtons extends StatelessWidget { ); } - void _onPromote(BuildContext context) => - context.read().add( - UserDashboardRoleChanged( - userId: user.id, - dashboardRole: DashboardUserRole.publisher, - ), - ); + void _onPromote(BuildContext context) { + showDialog( + context: context, + builder: (dialogContext) => ConfirmationDialog( + title: l10n.confirmPromotionTitle, + content: l10n.confirmPromotionMessage(user.email), + confirmText: l10n.promoteToPublisher, + onConfirm: () { + context.read().add( + UserDashboardRoleChanged( + userId: user.id, + dashboardRole: DashboardUserRole.publisher, + ), + ); + }, + ), + ); + } - void _onDemote(BuildContext context) => - context.read().add( - UserDashboardRoleChanged( - userId: user.id, - dashboardRole: DashboardUserRole.none, - ), - ); + void _onDemote(BuildContext context) { + showDialog( + context: context, + builder: (dialogContext) => ConfirmationDialog( + title: l10n.confirmDemotionTitle, + content: l10n.confirmDemotionMessage(user.email), + confirmText: l10n.demoteToUser, + onConfirm: () { + context.read().add( + UserDashboardRoleChanged( + userId: user.id, + dashboardRole: DashboardUserRole.none, + ), + ); + }, + ), + ); + } void _onCopyId(BuildContext context) { Clipboard.setData(ClipboardData(text: user.id)); From d50c2bb0934981541ee17c928fa55a1767b58a42 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 06:30:30 +0100 Subject: [PATCH 13/32] refactor(user_filter_dialog): update state to use filter enums - Replace selectedAppRoles and selectedDashboardRoles with individual filter fields - Add AuthenticationFilter, SubscriptionFilter, and dashboardRole - Update copyWith method and props list to reflect changes --- .../bloc/user_filter_dialog_state.dart | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/lib/user_management/widgets/user_filter_dialog/bloc/user_filter_dialog_state.dart b/lib/user_management/widgets/user_filter_dialog/bloc/user_filter_dialog_state.dart index 7bc8edce..cd258bd0 100644 --- a/lib/user_management/widgets/user_filter_dialog/bloc/user_filter_dialog_state.dart +++ b/lib/user_management/widgets/user_filter_dialog/bloc/user_filter_dialog_state.dart @@ -10,37 +10,43 @@ final class UserFilterDialogState extends Equatable { /// {@macro user_filter_dialog_state} const UserFilterDialogState({ this.searchQuery = '', - this.selectedAppRoles = const [], - this.selectedDashboardRoles = const [], + this.authenticationFilter = AuthenticationFilter.all, + this.subscriptionFilter = SubscriptionFilter.all, + this.dashboardRole, }); /// The current text in the search query field. final String searchQuery; - /// The list of app roles to be included in the filter. - final List selectedAppRoles; + /// The selected authentication status filter. + final AuthenticationFilter authenticationFilter; - /// The list of dashboard roles to be included in the filter. - final List selectedDashboardRoles; + /// The selected subscription status filter. + final SubscriptionFilter subscriptionFilter; + + /// The selected dashboard role filter. + final DashboardUserRole? dashboardRole; /// Creates a copy of this [UserFilterDialogState] with updated values. UserFilterDialogState copyWith({ String? searchQuery, - List? selectedAppRoles, - List? selectedDashboardRoles, + AuthenticationFilter? authenticationFilter, + SubscriptionFilter? subscriptionFilter, + DashboardUserRole? dashboardRole, }) { return UserFilterDialogState( searchQuery: searchQuery ?? this.searchQuery, - selectedAppRoles: selectedAppRoles ?? this.selectedAppRoles, - selectedDashboardRoles: - selectedDashboardRoles ?? this.selectedDashboardRoles, + authenticationFilter: authenticationFilter ?? this.authenticationFilter, + subscriptionFilter: subscriptionFilter ?? this.subscriptionFilter, + dashboardRole: dashboardRole ?? this.dashboardRole, ); } @override List get props => [ searchQuery, - selectedAppRoles, - selectedDashboardRoles, + authenticationFilter, + subscriptionFilter, + dashboardRole, ]; } From 288e6dbd89880c3a1604753f1260a37f5d227e21 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 06:30:45 +0100 Subject: [PATCH 14/32] refactor(user_management): update user filter dialog events - Rename and update filter-related events - Introduce new events for authentication, subscription, and dashboard role filters - Update existing events to accommodate new filter types */ - Remove UserFilterDialogAppRolesChanged event - Remove UserFilterDialogDashboardRolesChanged event - Add UserFilterDialogAuthenticationChanged event - Add UserFilterDialogSubscriptionChanged event - Add UserFilterDialogDashboardRoleChanged event --- .../bloc/user_filter_dialog_event.dart | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/lib/user_management/widgets/user_filter_dialog/bloc/user_filter_dialog_event.dart b/lib/user_management/widgets/user_filter_dialog/bloc/user_filter_dialog_event.dart index eed34339..1e0da355 100644 --- a/lib/user_management/widgets/user_filter_dialog/bloc/user_filter_dialog_event.dart +++ b/lib/user_management/widgets/user_filter_dialog/bloc/user_filter_dialog_event.dart @@ -30,25 +30,35 @@ final class UserFilterDialogSearchQueryChanged extends UserFilterDialogEvent { List get props => [query]; } -/// Event to update the temporary selected app roles in the dialog. -final class UserFilterDialogAppRolesChanged extends UserFilterDialogEvent { - const UserFilterDialogAppRolesChanged(this.appRoles); +/// Event to update the temporary authentication filter in the dialog. +final class UserFilterDialogAuthenticationChanged + extends UserFilterDialogEvent { + const UserFilterDialogAuthenticationChanged(this.authenticationFilter); - final List appRoles; + final AuthenticationFilter authenticationFilter; @override - List get props => [appRoles]; + List get props => [authenticationFilter]; } -/// Event to update the temporary selected dashboard roles in the dialog. -final class UserFilterDialogDashboardRolesChanged - extends UserFilterDialogEvent { - const UserFilterDialogDashboardRolesChanged(this.dashboardRoles); +/// Event to update the temporary subscription filter in the dialog. +final class UserFilterDialogSubscriptionChanged extends UserFilterDialogEvent { + const UserFilterDialogSubscriptionChanged(this.subscriptionFilter); + + final SubscriptionFilter subscriptionFilter; + + @override + List get props => [subscriptionFilter]; +} + +/// Event to update the temporary dashboard role filter in the dialog. +final class UserFilterDialogDashboardRoleChanged extends UserFilterDialogEvent { + const UserFilterDialogDashboardRoleChanged(this.dashboardRole); - final List dashboardRoles; + final DashboardUserRole? dashboardRole; @override - List get props => [dashboardRoles]; + List get props => [dashboardRole]; } /// Event to reset all temporary filter selections in the dialog. From 30ae36f138bd6da0bb862ed3dfe22de736da77bd Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 06:31:48 +0100 Subject: [PATCH 15/32] refactor(user_management): update UserFilterDialogBloc with new filters - Replace appRoles and dashboardRoles with authenticationFilter, subscriptionFilter, and dashboardRole - Update event handlers to reflect new filter structure - Implement toggle functionality for dashboardRole - Update state initialization to use new filter types --- .../bloc/user_filter_dialog_bloc.dart | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/lib/user_management/widgets/user_filter_dialog/bloc/user_filter_dialog_bloc.dart b/lib/user_management/widgets/user_filter_dialog/bloc/user_filter_dialog_bloc.dart index e04fe798..449e6494 100644 --- a/lib/user_management/widgets/user_filter_dialog/bloc/user_filter_dialog_bloc.dart +++ b/lib/user_management/widgets/user_filter_dialog/bloc/user_filter_dialog_bloc.dart @@ -2,6 +2,8 @@ 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/user_management/bloc/user_filter/user_filter_bloc.dart'; +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'; import 'package:flutter_news_app_web_dashboard_full_source_code/user_management/widgets/user_filter_dialog/user_filter_dialog.dart' show UserFilterDialog; @@ -22,9 +24,10 @@ class UserFilterDialogBloc UserFilterDialogBloc() : super(const UserFilterDialogState()) { on(_onFilterDialogInitialized); on(_onSearchQueryChanged); - on(_onAppRolesChanged); - on(_onDashboardRolesChanged); on(_onFilterDialogReset); + on(_onAuthenticationChanged); + on(_onSubscriptionChanged); + on(_onDashboardRoleChanged); } /// Initializes the dialog's state from the main [UserFilterBloc]'s state. @@ -35,8 +38,9 @@ class UserFilterDialogBloc emit( state.copyWith( searchQuery: event.userFilterState.searchQuery, - selectedAppRoles: event.userFilterState.selectedAppRoles, - selectedDashboardRoles: event.userFilterState.selectedDashboardRoles, + authenticationFilter: event.userFilterState.authenticationFilter, + subscriptionFilter: event.userFilterState.subscriptionFilter, + dashboardRole: event.userFilterState.dashboardRole, ), ); } @@ -49,20 +53,29 @@ class UserFilterDialogBloc emit(state.copyWith(searchQuery: event.query)); } - /// Updates the temporary selected app roles. - void _onAppRolesChanged( - UserFilterDialogAppRolesChanged event, + void _onAuthenticationChanged( + UserFilterDialogAuthenticationChanged event, Emitter emit, ) { - emit(state.copyWith(selectedAppRoles: event.appRoles)); + emit(state.copyWith(authenticationFilter: event.authenticationFilter)); } - /// Updates the temporary selected dashboard roles. - void _onDashboardRolesChanged( - UserFilterDialogDashboardRolesChanged event, + void _onSubscriptionChanged( + UserFilterDialogSubscriptionChanged event, Emitter emit, ) { - emit(state.copyWith(selectedDashboardRoles: event.dashboardRoles)); + emit(state.copyWith(subscriptionFilter: event.subscriptionFilter)); + } + + void _onDashboardRoleChanged( + UserFilterDialogDashboardRoleChanged event, + Emitter emit, + ) { + if (state.dashboardRole == event.dashboardRole) { + emit(state.copyWith(dashboardRole: null)); + } else { + emit(state.copyWith(dashboardRole: event.dashboardRole)); + } } /// Resets all temporary filter selections in the dialog. From 855d14bc0cdb9fa3be31a9ec3a61a24b2f0c4d99 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 06:33:00 +0100 Subject: [PATCH 16/32] refactor(user_management): revamp user filter dialog - Replace multi-select filters with single selection filters - Add authentication, subscription, and dashboard role filters - Implement _FilterSection widget for consistent filter UI - Update UserFilterApplied event with new filter parameters - Remove unused imports and organize dependencies --- .../user_filter_dialog.dart | 133 +++++++++++++----- 1 file changed, 99 insertions(+), 34 deletions(-) diff --git a/lib/user_management/widgets/user_filter_dialog/user_filter_dialog.dart b/lib/user_management/widgets/user_filter_dialog/user_filter_dialog.dart index cb203187..27c5a54d 100644 --- a/lib/user_management/widgets/user_filter_dialog/user_filter_dialog.dart +++ b/lib/user_management/widgets/user_filter_dialog/user_filter_dialog.dart @@ -2,10 +2,10 @@ import 'package:core/core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/app_user_role_l10n.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/dashboard_user_role_l10n.dart'; -import 'package:flutter_news_app_web_dashboard_full_source_code/shared/widgets/searchable_selection_input.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/shared.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/user_management/bloc/user_filter/user_filter_bloc.dart'; +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'; import 'package:flutter_news_app_web_dashboard_full_source_code/user_management/widgets/user_filter_dialog/bloc/user_filter_dialog_bloc.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -45,8 +45,9 @@ class _UserFilterDialogState extends State { context.read().add( UserFilterApplied( searchQuery: filterDialogState.searchQuery, - selectedAppRoles: filterDialogState.selectedAppRoles, - selectedDashboardRoles: filterDialogState.selectedDashboardRoles, + authenticationFilter: filterDialogState.authenticationFilter, + subscriptionFilter: filterDialogState.subscriptionFilter, + dashboardRole: filterDialogState.dashboardRole, ), ); } @@ -116,40 +117,51 @@ class _UserFilterDialogState extends State { ), const SizedBox(height: AppSpacing.lg), - // Filter for AppUserRole. - SearchableSelectionInput( - label: l10n.appRole, - hintText: l10n.selectAppRoles, - isMultiSelect: true, - selectedItems: filterDialogState.selectedAppRoles, - itemBuilder: (context, item) => Text(item.l10n(context)), - itemToString: (item) => item.l10n(context), - onChanged: (items) { - context.read().add( - UserFilterDialogAppRolesChanged(items ?? []), - ); - }, - staticItems: AppUserRole.values, + // Authentication Filter + _FilterSection( + title: l10n.authentication, + selectedValue: filterDialogState.authenticationFilter, + values: AuthenticationFilter.values, + onSelected: (value) => + context.read().add( + UserFilterDialogAuthenticationChanged( + value, + ), + ), + chipLabelBuilder: (value) => value.l10n(context), ), const SizedBox(height: AppSpacing.lg), - // Filter for DashboardUserRole. - SearchableSelectionInput( - label: l10n.dashboardRole, - hintText: l10n.selectDashboardRoles, - isMultiSelect: true, - selectedItems: filterDialogState.selectedDashboardRoles, - itemBuilder: (context, item) => Text(item.l10n(context)), - itemToString: (item) => item.l10n(context), - onChanged: (items) { - context.read().add( - UserFilterDialogDashboardRolesChanged(items ?? []), - ); - }, - // Exclude 'admin' from the selectable roles in the filter. - staticItems: DashboardUserRole.values + // Subscription Filter + _FilterSection( + title: l10n.subscription, + selectedValue: filterDialogState.subscriptionFilter, + values: SubscriptionFilter.values, + onSelected: (value) => + context.read().add( + UserFilterDialogSubscriptionChanged( + value, + ), + ), + chipLabelBuilder: (value) => value.l10n(context), + ), + const SizedBox(height: AppSpacing.lg), + + // Dashboard Role Filter + _FilterSection( + title: l10n.dashboardRole, + selectedValue: filterDialogState.dashboardRole, + values: DashboardUserRole.values .where((role) => role != DashboardUserRole.admin) .toList(), + onSelected: (value) => + context.read().add( + UserFilterDialogDashboardRoleChanged( + value as DashboardUserRole?, + ), + ), + chipLabelBuilder: (value) => value.l10n(context), + includeAllOption: true, ), ], ), @@ -160,3 +172,56 @@ class _UserFilterDialogState extends State { ); } } + +class _FilterSection extends StatelessWidget { + const _FilterSection({ + required this.title, + required this.selectedValue, + required this.values, + required this.onSelected, + required this.chipLabelBuilder, + this.includeAllOption = false, + }); + + final String title; + final T? selectedValue; + final List values; + final ValueChanged onSelected; + final String Function(T) chipLabelBuilder; + final bool includeAllOption; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizationsX(context).l10n; + final textTheme = Theme.of(context).textTheme; + + final allValues = includeAllOption + ? [null, ...values.where((v) => v != null)] + : values; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: textTheme.titleMedium), + const SizedBox(height: AppSpacing.md), + Wrap( + spacing: AppSpacing.sm, + runSpacing: AppSpacing.sm, + children: allValues.map((value) { + final isSelected = selectedValue == value; + final label = value == null + ? l10n.any + : chipLabelBuilder(value as T); + return ChoiceChip( + label: Text(label), + selected: isSelected, + onSelected: (_) { + onSelected(value as T); + }, + ); + }).toList(), + ), + ], + ); + } +} From 8c2d0017d2ec1f5a945fc90077ee0d12804dd034 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 06:35:34 +0100 Subject: [PATCH 17/32] chore: barrels --- .../selection_page/selection_page.dart | 2 + lib/shared/widgets/widgets.dart | 6 +- lib/user_management/view/users_page.dart | 82 ++++++++++++------- 3 files changed, 58 insertions(+), 32 deletions(-) create mode 100644 lib/shared/widgets/selection_page/selection_page.dart diff --git a/lib/shared/widgets/selection_page/selection_page.dart b/lib/shared/widgets/selection_page/selection_page.dart new file mode 100644 index 00000000..8b8deea6 --- /dev/null +++ b/lib/shared/widgets/selection_page/selection_page.dart @@ -0,0 +1,2 @@ +export 'searchable_selection_page.dart'; +export 'selection_page_arguments.dart'; diff --git a/lib/shared/widgets/widgets.dart b/lib/shared/widgets/widgets.dart index 311cf333..e3b9f66d 100644 --- a/lib/shared/widgets/widgets.dart +++ b/lib/shared/widgets/widgets.dart @@ -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'; diff --git a/lib/user_management/view/users_page.dart b/lib/user_management/view/users_page.dart index b3490c3f..110304e2 100644 --- a/lib/user_management/view/users_page.dart +++ b/lib/user_management/view/users_page.dart @@ -4,8 +4,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/l10n.dart'; +import 'package:flutter_news_app_web_dashboard_full_source_code/shared/extensions/app_user_role_ui.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/user_management/bloc/user_filter/user_filter_bloc.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/user_management/bloc/user_management_bloc.dart'; +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'; +import 'package:flutter_news_app_web_dashboard_full_source_code/user_management/view/dashboard_user_role_ui.dart'; import 'package:flutter_news_app_web_dashboard_full_source_code/user_management/widgets/user_action_buttons.dart'; import 'package:intl/intl.dart'; import 'package:ui_kit/ui_kit.dart'; @@ -41,7 +45,9 @@ class _UsersPageState extends State { /// Checks if any filters are currently active in the UserFilterBloc. bool _areFiltersActive(UserFilterState state) { - return state.searchQuery.isNotEmpty || state.selectedAppRoles.isNotEmpty; + return state.searchQuery.isNotEmpty || + state.authenticationFilter != AuthenticationFilter.all || + state.subscriptionFilter != SubscriptionFilter.all; } @override @@ -136,13 +142,9 @@ class _UsersPageState extends State { ), if (!isMobile) DataColumn2( - label: Text(l10n.subscription), + label: Text(l10n.createdAt), size: ColumnSize.S, ), - DataColumn2( - label: Text(l10n.createdAt), - size: ColumnSize.S, - ), DataColumn2( label: Text(l10n.actions), size: ColumnSize.S, @@ -224,13 +226,31 @@ class _UsersDataSource extends DataTableSource { // The email cell is wrapped in an Expanded widget to allow truncation. cells: [ DataCell( - Row( + Stack( + alignment: Alignment.center, children: [ - Expanded( - child: Text( - user.email, - overflow: TextOverflow.ellipsis, - maxLines: 1, + // The email text is padded to prevent the indicator dots from + // overlapping the text content. + Padding( + padding: const EdgeInsets.only(right: 24), + child: Text(user.email, overflow: TextOverflow.ellipsis), + ), + Positioned( + right: 0, + top: 0, + bottom: 0, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Premium subscription indicator dot (gold) + if (user.appRole.isPremium) + const _IndicatorDot(color: Colors.amber), + // Privileged dashboard role indicator dot (blue) + if (user.dashboardRole.isPrivileged) ...[ + const SizedBox(width: AppSpacing.xs), + const _IndicatorDot(color: Colors.blueAccent), + ], + ], ), ), ], @@ -238,8 +258,6 @@ class _UsersDataSource extends DataTableSource { ), if (!isMobile) DataCell(Text(user.appRole.authenticationStatusL10n(context))), - if (!isMobile) - DataCell(Text(user.appRole.subscriptionStatusL10n(context))), DataCell( Text( DateFormat('dd-MM-yyyy').format(user.createdAt.toLocal()), @@ -265,6 +283,26 @@ class _UsersDataSource extends DataTableSource { int get selectedRowCount => 0; } +/// A small, colored dot used as a visual indicator. +class _IndicatorDot extends StatelessWidget { + const _IndicatorDot({required this.color}); + + /// The color of the dot. + final Color color; + + @override + Widget build(BuildContext context) { + return Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + ); + } +} + /// An extension to get the localized string for the authentication status /// derived from [AppUserRole]. extension AuthenticationStatusL10n on AppUserRole { @@ -280,19 +318,3 @@ extension AuthenticationStatusL10n on AppUserRole { } } } - -/// An extension to get the localized string for the subscription status -/// derived from [AppUserRole]. -extension SubscriptionStatusL10n on AppUserRole { - /// Returns the localized subscription status string. - String subscriptionStatusL10n(BuildContext context) { - final l10n = AppLocalizationsX(context).l10n; - switch (this) { - case AppUserRole.guestUser: - case AppUserRole.standardUser: - return l10n.subscriptionFree; - case AppUserRole.premiumUser: - return l10n.subscriptionPremium; - } - } -} From 84a41432fa9b6af15cfbb6a8839e5df97654759a Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 06:49:59 +0100 Subject: [PATCH 18/32] feat(l10n): add tooltips for premium and privileged users - Add Arabic and English translations for premium user and privileged user tooltips - Include descriptions for new localization strings in both languages --- lib/l10n/arb/app_ar.arb | 8 ++++++++ lib/l10n/arb/app_en.arb | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index d499bed8..6d8d41f1 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -2724,5 +2724,13 @@ "type": "String" } } + }, + "premiumUserTooltip": "مستخدم مميز", + "@premiumUserTooltip": { + "description": "تلميح للأيقونة التي تشير إلى مستخدم مميز." + }, + "privilegedUserTooltip": "ناشر أو مسؤول في لوحة التحكم", + "@privilegedUserTooltip": { + "description": "تلميح للأيقونة التي تشير إلى مستخدم لديه وصول مميز إلى لوحة التحكم." } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 0a514192..5d923e84 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2720,5 +2720,13 @@ "type": "String" } } + }, + "premiumUserTooltip": "Premium User", + "@premiumUserTooltip": { + "description": "Tooltip for the icon indicating a premium user." + }, + "privilegedUserTooltip": "Dashboard Publisher or Admin", + "@privilegedUserTooltip": { + "description": "Tooltip for the icon indicating a user with privileged dashboard access." } } \ No newline at end of file From 2f8d681866945c993c1fc324fecc8e878ab9d729 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 06:50:29 +0100 Subject: [PATCH 19/32] build(l10n): sync. --- lib/l10n/app_localizations.dart | 12 ++++++++++++ lib/l10n/app_localizations_ar.dart | 6 ++++++ lib/l10n/app_localizations_en.dart | 6 ++++++ 3 files changed, 24 insertions(+) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index f0f7183e..3ac8268d 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4021,6 +4021,18 @@ abstract class AppLocalizations { /// 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 User'** + String get premiumUserTooltip; + + /// Tooltip for the icon indicating a user with privileged dashboard access. + /// + /// In en, this message translates to: + /// **'Dashboard Publisher or Admin'** + String get privilegedUserTooltip; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 81f0e38e..fff9e22a 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -2158,4 +2158,10 @@ class AppLocalizationsAr extends AppLocalizations { String confirmDemotionMessage(String email) { return 'هل أنت متأكد أنك تريد تخفيض رتبة $email إلى مستخدم عادي؟'; } + + @override + String get premiumUserTooltip => 'مستخدم مميز'; + + @override + String get privilegedUserTooltip => 'ناشر أو مسؤول في لوحة التحكم'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 20c92458..b3d6e3e1 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2164,4 +2164,10 @@ class AppLocalizationsEn extends AppLocalizations { String confirmDemotionMessage(String email) { return 'Are you sure you want to demote $email to a standard user?'; } + + @override + String get premiumUserTooltip => 'Premium User'; + + @override + String get privilegedUserTooltip => 'Dashboard Publisher or Admin'; } From 09122de221e4f89410fb0c4bca709a1382088845 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 06:50:35 +0100 Subject: [PATCH 20/32] refactor(user_management): replace indicator dots with tooltip icons - Remove _IndicatorDot widget - Replace premium user dot with amber star icon - Replace privileged user dot with blue shield icon - Add tooltips for both premium and privileged user icons --- lib/user_management/view/users_page.dart | 38 ++++++++++-------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/lib/user_management/view/users_page.dart b/lib/user_management/view/users_page.dart index 110304e2..1af19bac 100644 --- a/lib/user_management/view/users_page.dart +++ b/lib/user_management/view/users_page.dart @@ -244,11 +244,25 @@ class _UsersDataSource extends DataTableSource { children: [ // Premium subscription indicator dot (gold) if (user.appRole.isPremium) - const _IndicatorDot(color: Colors.amber), + Tooltip( + message: l10n.premiumUserTooltip, + child: const Icon( + Icons.star, + color: Colors.amber, + size: 16, + ), + ), // Privileged dashboard role indicator dot (blue) if (user.dashboardRole.isPrivileged) ...[ const SizedBox(width: AppSpacing.xs), - const _IndicatorDot(color: Colors.blueAccent), + Tooltip( + message: l10n.privilegedUserTooltip, + child: const Icon( + Icons.shield, + color: Colors.blueAccent, + size: 16, + ), + ), ], ], ), @@ -283,26 +297,6 @@ class _UsersDataSource extends DataTableSource { int get selectedRowCount => 0; } -/// A small, colored dot used as a visual indicator. -class _IndicatorDot extends StatelessWidget { - const _IndicatorDot({required this.color}); - - /// The color of the dot. - final Color color; - - @override - Widget build(BuildContext context) { - return Container( - width: 8, - height: 8, - decoration: BoxDecoration( - color: color, - shape: BoxShape.circle, - ), - ); - } -} - /// An extension to get the localized string for the authentication status /// derived from [AppUserRole]. extension AuthenticationStatusL10n on AppUserRole { From 20c84ca0306b56e962300d808321153c4915f98c Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 07:03:56 +0100 Subject: [PATCH 21/32] feat(l10n): improve user role tooltips and translations - Split "privilegedUserTooltip" into "adminUserTooltip" and "publisherUserTooltip" for more granular role definitions - Shorten "premiumUserTooltip" text in English for better icon label visibility - Update Arabic translations for new tooltip texts - Adjust tooltip descriptions in both languages for clarity and consistency --- lib/l10n/arb/app_ar.arb | 10 +++++++--- lib/l10n/arb/app_en.arb | 12 ++++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index 6d8d41f1..cd58a70c 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -2729,8 +2729,12 @@ "@premiumUserTooltip": { "description": "تلميح للأيقونة التي تشير إلى مستخدم مميز." }, - "privilegedUserTooltip": "ناشر أو مسؤول في لوحة التحكم", - "@privilegedUserTooltip": { - "description": "تلميح للأيقونة التي تشير إلى مستخدم لديه وصول مميز إلى لوحة التحكم." + "adminUserTooltip": "مسؤول", + "@adminUserTooltip": { + "description": "تلميح للأيقونة التي تشير إلى مستخدم مسؤول." + }, + "publisherUserTooltip": "ناشر", + "@publisherUserTooltip": { + "description": "تلميح للأيقونة التي تشير إلى مستخدم ناشر." } } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 5d923e84..028171e5 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -2721,12 +2721,16 @@ } } }, - "premiumUserTooltip": "Premium User", + "premiumUserTooltip": "Premium", "@premiumUserTooltip": { "description": "Tooltip for the icon indicating a premium user." }, - "privilegedUserTooltip": "Dashboard Publisher or Admin", - "@privilegedUserTooltip": { - "description": "Tooltip for the icon indicating a user with privileged dashboard access." + "adminUserTooltip": "Admin", + "@adminUserTooltip": { + "description": "Tooltip for the icon indicating an admin user." + }, + "publisherUserTooltip": "Publisher", + "@publisherUserTooltip": { + "description": "Tooltip for the icon indicating a publisher user." } } \ No newline at end of file From 18ec2c1a7dd049680c3ee7bdc87f86c67b22d72a Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 07:04:09 +0100 Subject: [PATCH 22/32] build(l10n): sync --- lib/l10n/app_localizations.dart | 14 ++++++++++---- lib/l10n/app_localizations_ar.dart | 5 ++++- lib/l10n/app_localizations_en.dart | 7 +++++-- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 3ac8268d..427cd04c 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4025,14 +4025,20 @@ abstract class AppLocalizations { /// Tooltip for the icon indicating a premium user. /// /// In en, this message translates to: - /// **'Premium User'** + /// **'Premium'** String get premiumUserTooltip; - /// Tooltip for the icon indicating a user with privileged dashboard access. + /// Tooltip for the icon indicating an admin user. /// /// In en, this message translates to: - /// **'Dashboard Publisher or Admin'** - String get privilegedUserTooltip; + /// **'Admin'** + String get adminUserTooltip; + + /// Tooltip for the icon indicating a publisher user. + /// + /// In en, this message translates to: + /// **'Publisher'** + String get publisherUserTooltip; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index fff9e22a..a5514382 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -2163,5 +2163,8 @@ class AppLocalizationsAr extends AppLocalizations { String get premiumUserTooltip => 'مستخدم مميز'; @override - String get privilegedUserTooltip => 'ناشر أو مسؤول في لوحة التحكم'; + String get adminUserTooltip => 'مسؤول'; + + @override + String get publisherUserTooltip => 'ناشر'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index b3d6e3e1..ab2c3add 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2166,8 +2166,11 @@ class AppLocalizationsEn extends AppLocalizations { } @override - String get premiumUserTooltip => 'Premium User'; + String get premiumUserTooltip => 'Premium'; @override - String get privilegedUserTooltip => 'Dashboard Publisher or Admin'; + String get adminUserTooltip => 'Admin'; + + @override + String get publisherUserTooltip => 'Publisher'; } From 6385ec4da2eff58d125e00e280886c55f5ff7251 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 07:13:07 +0100 Subject: [PATCH 23/32] feat(user_management): add dashboard role filter functionality - Implement filtering by dashboard role in user management - Add conditional logic to apply filter only when a specific role is selected - Ensure 'Any' selection does not apply any filters --- lib/user_management/bloc/user_management_bloc.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/user_management/bloc/user_management_bloc.dart b/lib/user_management/bloc/user_management_bloc.dart index 955d2650..80164fa0 100644 --- a/lib/user_management/bloc/user_management_bloc.dart +++ b/lib/user_management/bloc/user_management_bloc.dart @@ -129,6 +129,14 @@ class UserManagementBloc filter['appRole'] = {r'$in': intersectingRoles.toList()}; } + if (state.dashboardRole != null) { + // When 'Any' is selected, dashboardRole is null, so we don't add the filter. + // Otherwise, we filter by the specific role name. + if (state.dashboardRole != DashboardUserRole.none) { + filter['dashboardRole'] = state.dashboardRole!.name; + } + } + return filter; } From 3f8f9df7d31babd4e7ad3ec41978df02edc81858 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 07:13:18 +0100 Subject: [PATCH 24/32] fix(user-management): wrap user email in SingleChildScrollView for scrolling - Replace Stack with SingleChildScrollView to allow horizontal scrolling for long email addresses - Remove positional dots and replace with horizontal layout - Add icons for premium and privileged users with appropriate tooltips - Improve layout for admin and publisher indicators --- lib/user_management/view/users_page.dart | 76 +++++++++++------------- 1 file changed, 36 insertions(+), 40 deletions(-) diff --git a/lib/user_management/view/users_page.dart b/lib/user_management/view/users_page.dart index 1af19bac..5bdd0879 100644 --- a/lib/user_management/view/users_page.dart +++ b/lib/user_management/view/users_page.dart @@ -226,48 +226,44 @@ class _UsersDataSource extends DataTableSource { // The email cell is wrapped in an Expanded widget to allow truncation. cells: [ DataCell( - Stack( - alignment: Alignment.center, - children: [ - // The email text is padded to prevent the indicator dots from - // overlapping the text content. - Padding( - padding: const EdgeInsets.only(right: 24), - child: Text(user.email, overflow: TextOverflow.ellipsis), - ), - Positioned( - right: 0, - top: 0, - bottom: 0, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - // Premium subscription indicator dot (gold) - if (user.appRole.isPremium) - Tooltip( - message: l10n.premiumUserTooltip, - child: const Icon( - Icons.star, - color: Colors.amber, - size: 16, - ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + Text(user.email), + const SizedBox(width: AppSpacing.sm), + if (user.appRole.isPremium) + Tooltip( + message: l10n.premiumUserTooltip, + child: const Icon( + Icons.star, + color: Colors.amber, + size: 16, + ), + ), + if (user.dashboardRole.isPrivileged) ...[ + const SizedBox(width: AppSpacing.xs), + if (user.dashboardRole == DashboardUserRole.admin) + Tooltip( + message: l10n.adminUserTooltip, + child: const Icon( + Icons.admin_panel_settings, + color: Colors.blueAccent, + size: 16, ), - // Privileged dashboard role indicator dot (blue) - if (user.dashboardRole.isPrivileged) ...[ - const SizedBox(width: AppSpacing.xs), - Tooltip( - message: l10n.privilegedUserTooltip, - child: const Icon( - Icons.shield, - color: Colors.blueAccent, - size: 16, - ), + ) + else if (user.dashboardRole == DashboardUserRole.publisher) + Tooltip( + message: l10n.publisherUserTooltip, + child: const Icon( + Icons.publish, + color: Colors.green, + size: 16, ), - ], - ], - ), - ), - ], + ), + ], + ], + ), ), ), if (!isMobile) From 2f0131916e3ad7fa58c231a02a2b939f42ec28b4 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 07:19:15 +0100 Subject: [PATCH 25/32] fix(user_management): remove redundant role filter check - Remove unnecessary condition for DashboardUserRole.none - Directly assign dashboardRole to filter when it's not null --- lib/user_management/bloc/user_management_bloc.dart | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/user_management/bloc/user_management_bloc.dart b/lib/user_management/bloc/user_management_bloc.dart index 80164fa0..212a4f0d 100644 --- a/lib/user_management/bloc/user_management_bloc.dart +++ b/lib/user_management/bloc/user_management_bloc.dart @@ -130,11 +130,7 @@ class UserManagementBloc } if (state.dashboardRole != null) { - // When 'Any' is selected, dashboardRole is null, so we don't add the filter. - // Otherwise, we filter by the specific role name. - if (state.dashboardRole != DashboardUserRole.none) { - filter['dashboardRole'] = state.dashboardRole!.name; - } + filter['dashboardRole'] = state.dashboardRole!.name; } return filter; From ca35dd94d092c5101d0b447715ff9bb9667793f6 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 07:19:23 +0100 Subject: [PATCH 26/32] fix(user_management): update user filter dialog - Add null safety to authentication and subscription filters - Update dashboard role filter values and selection logic - Modify filter section to handle nullable values consistently --- .../user_filter_dialog/user_filter_dialog.dart | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/user_management/widgets/user_filter_dialog/user_filter_dialog.dart b/lib/user_management/widgets/user_filter_dialog/user_filter_dialog.dart index 27c5a54d..519b398b 100644 --- a/lib/user_management/widgets/user_filter_dialog/user_filter_dialog.dart +++ b/lib/user_management/widgets/user_filter_dialog/user_filter_dialog.dart @@ -125,7 +125,7 @@ class _UserFilterDialogState extends State { onSelected: (value) => context.read().add( UserFilterDialogAuthenticationChanged( - value, + value!, ), ), chipLabelBuilder: (value) => value.l10n(context), @@ -140,7 +140,7 @@ class _UserFilterDialogState extends State { onSelected: (value) => context.read().add( UserFilterDialogSubscriptionChanged( - value, + value!, ), ), chipLabelBuilder: (value) => value.l10n(context), @@ -151,13 +151,15 @@ class _UserFilterDialogState extends State { _FilterSection( title: l10n.dashboardRole, selectedValue: filterDialogState.dashboardRole, - values: DashboardUserRole.values - .where((role) => role != DashboardUserRole.admin) - .toList(), + values: const [ + DashboardUserRole.admin, + DashboardUserRole.publisher, + DashboardUserRole.none, + ], onSelected: (value) => context.read().add( UserFilterDialogDashboardRoleChanged( - value as DashboardUserRole?, + value, ), ), chipLabelBuilder: (value) => value.l10n(context), @@ -186,7 +188,7 @@ class _FilterSection extends StatelessWidget { final String title; final T? selectedValue; final List values; - final ValueChanged onSelected; + final ValueChanged onSelected; final String Function(T) chipLabelBuilder; final bool includeAllOption; @@ -216,7 +218,7 @@ class _FilterSection extends StatelessWidget { label: Text(label), selected: isSelected, onSelected: (_) { - onSelected(value as T); + onSelected(value); }, ); }).toList(), From 81ff7a2349ae4f89c1b4edd5db06bf70cd110e55 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 07:19:35 +0100 Subject: [PATCH 27/32] refactor(user-management): improve code readability in UserFilterDialogBloc - Enhance the logic for toggling dashboard role selection - Increase code readability with the addition of comments and restructuring conditional logic - Optimize variable assignment for clarity --- .../bloc/user_filter_dialog_bloc.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/user_management/widgets/user_filter_dialog/bloc/user_filter_dialog_bloc.dart b/lib/user_management/widgets/user_filter_dialog/bloc/user_filter_dialog_bloc.dart index 449e6494..26b0c197 100644 --- a/lib/user_management/widgets/user_filter_dialog/bloc/user_filter_dialog_bloc.dart +++ b/lib/user_management/widgets/user_filter_dialog/bloc/user_filter_dialog_bloc.dart @@ -71,11 +71,13 @@ class UserFilterDialogBloc UserFilterDialogDashboardRoleChanged event, Emitter emit, ) { - if (state.dashboardRole == event.dashboardRole) { - emit(state.copyWith(dashboardRole: null)); - } else { - emit(state.copyWith(dashboardRole: event.dashboardRole)); - } + // This logic allows toggling the selection. If the user clicks the + // currently selected role, it deselects it (sets to null), which + // corresponds to the 'Any' state. + final newRole = state.dashboardRole == event.dashboardRole + ? null + : event.dashboardRole; + emit(state.copyWith(dashboardRole: newRole)); } /// Resets all temporary filter selections in the dialog. From 9028e5af5d7223b713f341eb78448d488e73eeb5 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 07:31:01 +0100 Subject: [PATCH 28/32] fix(user-management): adjust role selection logic in user filter dialog - Remove toggling logic for dashboard role selection - Allow direct setting of role, including null value --- .../bloc/user_filter_dialog_bloc.dart | 10 +++------- .../widgets/user_filter_dialog/user_filter_dialog.dart | 1 - 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/user_management/widgets/user_filter_dialog/bloc/user_filter_dialog_bloc.dart b/lib/user_management/widgets/user_filter_dialog/bloc/user_filter_dialog_bloc.dart index 26b0c197..fa318111 100644 --- a/lib/user_management/widgets/user_filter_dialog/bloc/user_filter_dialog_bloc.dart +++ b/lib/user_management/widgets/user_filter_dialog/bloc/user_filter_dialog_bloc.dart @@ -71,13 +71,9 @@ class UserFilterDialogBloc UserFilterDialogDashboardRoleChanged event, Emitter emit, ) { - // This logic allows toggling the selection. If the user clicks the - // currently selected role, it deselects it (sets to null), which - // corresponds to the 'Any' state. - final newRole = state.dashboardRole == event.dashboardRole - ? null - : event.dashboardRole; - emit(state.copyWith(dashboardRole: newRole)); + // Directly set the state to the selected role. The UI's `onSelected` + // will pass `null` when 'Any' is tapped. + emit(state.copyWith(dashboardRole: event.dashboardRole)); } /// Resets all temporary filter selections in the dialog. diff --git a/lib/user_management/widgets/user_filter_dialog/user_filter_dialog.dart b/lib/user_management/widgets/user_filter_dialog/user_filter_dialog.dart index 519b398b..1345b409 100644 --- a/lib/user_management/widgets/user_filter_dialog/user_filter_dialog.dart +++ b/lib/user_management/widgets/user_filter_dialog/user_filter_dialog.dart @@ -154,7 +154,6 @@ class _UserFilterDialogState extends State { values: const [ DashboardUserRole.admin, DashboardUserRole.publisher, - DashboardUserRole.none, ], onSelected: (value) => context.read().add( From df5f1e9a78021149f7392f61e367f5c37b2a3c7d Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 07:45:23 +0100 Subject: [PATCH 29/32] feat(ui): wrap premium icon with tooltip for screen readers - Add AppLocalizations import for localization support - Rename getPremiumIcon method and add AppLocalizations parameter - Wrap premium icon with Tooltip widget for accessibility - Update tooltip message using localization --- lib/shared/extensions/app_user_role_ui.dart | 10 ++++--- .../view/dashboard_user_role_ui.dart | 28 +++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/lib/shared/extensions/app_user_role_ui.dart b/lib/shared/extensions/app_user_role_ui.dart index c3e57a70..97cd015d 100644 --- a/lib/shared/extensions/app_user_role_ui.dart +++ b/lib/shared/extensions/app_user_role_ui.dart @@ -1,18 +1,20 @@ 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 if the user is a premium user. + /// 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? get premiumIcon { + Widget? getPremiumIcon(AppLocalizations l10n) { if (isPremium) { - return const Padding( - padding: EdgeInsets.only(left: 8), + return Tooltip( + message: l10n.premiumUserTooltip, child: Icon(Icons.star, color: Colors.amber, size: 16), ); } diff --git a/lib/user_management/view/dashboard_user_role_ui.dart b/lib/user_management/view/dashboard_user_role_ui.dart index 5ff2fc16..a5fb1e69 100644 --- a/lib/user_management/view/dashboard_user_role_ui.dart +++ b/lib/user_management/view/dashboard_user_role_ui.dart @@ -1,4 +1,6 @@ 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 [DashboardUserRole] to provide UI-related helpers. extension DashboardUserRoleUI on DashboardUserRole { @@ -7,4 +9,30 @@ extension DashboardUserRoleUI on DashboardUserRole { return this == DashboardUserRole.admin || this == DashboardUserRole.publisher; } + + /// Returns a role indicator icon wrapped in a tooltip. + Widget? getRoleIcon(AppLocalizations l10n) { + switch (this) { + case DashboardUserRole.admin: + return Tooltip( + message: l10n.adminUserTooltip, + child: const Icon( + Icons.admin_panel_settings, + color: Colors.blueAccent, + size: 16, + ), + ); + case DashboardUserRole.publisher: + return Tooltip( + message: l10n.publisherUserTooltip, + child: const Icon( + Icons.publish, + color: Colors.green, + size: 16, + ), + ); + case DashboardUserRole.none: + return null; + } + } } From 6153f23dee7839bc17ddcb691081cb66f72adac6 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 07:45:50 +0100 Subject: [PATCH 30/32] refactor(user_management): remove unused buildFilterMap function - Remove the unused buildFilterMap method from the UserFilterBloc class - This function was likely intended to build a filter map for querying the data repository, but it is no longer needed in its current form --- .../bloc/user_filter/user_filter_bloc.dart | 44 ------------------- 1 file changed, 44 deletions(-) diff --git a/lib/user_management/bloc/user_filter/user_filter_bloc.dart b/lib/user_management/bloc/user_filter/user_filter_bloc.dart index 7e483fda..9a67f19e 100644 --- a/lib/user_management/bloc/user_filter/user_filter_bloc.dart +++ b/lib/user_management/bloc/user_filter/user_filter_bloc.dart @@ -54,48 +54,4 @@ class UserFilterBloc extends Bloc { ), ); } - - /// Builds the filter map for the data repository query. - Map buildFilterMap() { - final filter = {}; - - if (state.searchQuery.isNotEmpty) { - filter[r'$or'] = [ - { - 'email': {r'$regex': state.searchQuery, r'$options': 'i'}, - }, - {'_id': state.searchQuery}, - ]; - } - - final appRoleFilter = []; - switch (state.authenticationFilter) { - case AuthenticationFilter.authenticated: - appRoleFilter.addAll( - [AppUserRole.standardUser.name, AppUserRole.premiumUser.name], - ); - case AuthenticationFilter.anonymous: - appRoleFilter.add(AppUserRole.guestUser.name); - case AuthenticationFilter.all: - break; - } - - final subscriptionRoles = {}; - switch (state.subscriptionFilter) { - case SubscriptionFilter.premium: - subscriptionRoles.add(AppUserRole.premiumUser.name); - case SubscriptionFilter.free: - subscriptionRoles.addAll( - [AppUserRole.guestUser.name, AppUserRole.standardUser.name], - ); - case SubscriptionFilter.all: - break; - } - - if (appRoleFilter.isNotEmpty) { - filter['appRole'] = {r'$in': appRoleFilter.toList()}; - } - - return filter; - } } From a60e33bf4b819dc9dc3ba10e9216d121351ebfa7 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 07:46:02 +0100 Subject: [PATCH 31/32] refactor(users): improve email display and role icon logic - Replace SingleChildScrollView with Row and Flexible for email display - Add TextOverflow.ellipsis for long email addresses - Simplify premium and role icon display logic - Use getPremiumIcon and getRoleIcon methods for better maintainability --- lib/user_management/view/users_page.dart | 48 ++++++------------------ 1 file changed, 12 insertions(+), 36 deletions(-) diff --git a/lib/user_management/view/users_page.dart b/lib/user_management/view/users_page.dart index 5bdd0879..892eb816 100644 --- a/lib/user_management/view/users_page.dart +++ b/lib/user_management/view/users_page.dart @@ -226,44 +226,20 @@ class _UsersDataSource extends DataTableSource { // The email cell is wrapped in an Expanded widget to allow truncation. cells: [ DataCell( - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - Text(user.email), + Row( + children: [ + Flexible( + child: Text(user.email, overflow: TextOverflow.ellipsis), + ), + if (user.appRole.getPremiumIcon(l10n) case final icon?) ...[ const SizedBox(width: AppSpacing.sm), - if (user.appRole.isPremium) - Tooltip( - message: l10n.premiumUserTooltip, - child: const Icon( - Icons.star, - color: Colors.amber, - size: 16, - ), - ), - if (user.dashboardRole.isPrivileged) ...[ - const SizedBox(width: AppSpacing.xs), - if (user.dashboardRole == DashboardUserRole.admin) - Tooltip( - message: l10n.adminUserTooltip, - child: const Icon( - Icons.admin_panel_settings, - color: Colors.blueAccent, - size: 16, - ), - ) - else if (user.dashboardRole == DashboardUserRole.publisher) - Tooltip( - message: l10n.publisherUserTooltip, - child: const Icon( - Icons.publish, - color: Colors.green, - size: 16, - ), - ), - ], + icon, ], - ), + if (user.dashboardRole.getRoleIcon(l10n) case final icon?) ...[ + const SizedBox(width: AppSpacing.xs), + icon, + ], + ], ), ), if (!isMobile) From 6efae19f3e6e31e98c3f8f4a9403d8720dcd8a5e Mon Sep 17 00:00:00 2001 From: fulleni Date: Sat, 13 Dec 2025 07:46:25 +0100 Subject: [PATCH 32/32] style: format --- lib/shared/extensions/app_user_role_ui.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/shared/extensions/app_user_role_ui.dart b/lib/shared/extensions/app_user_role_ui.dart index 97cd015d..9a76a81f 100644 --- a/lib/shared/extensions/app_user_role_ui.dart +++ b/lib/shared/extensions/app_user_role_ui.dart @@ -15,7 +15,7 @@ extension AppUserRoleUI on AppUserRole { if (isPremium) { return Tooltip( message: l10n.premiumUserTooltip, - child: Icon(Icons.star, color: Colors.amber, size: 16), + child: const Icon(Icons.star, color: Colors.amber, size: 16), ); } return null;