From c3c4764f59b7bc0e31204ca7a03ea102cbcae0ca Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Thu, 13 Mar 2025 03:18:39 +0530 Subject: [PATCH 1/3] feat(status): Implement global status bar and request body in get validation - Create StatusMessage model with support for info/warning/error types - Design GlobalStatusBar UI component with dynamic styling based on status type and located at the bottom of the editorpane -Implement GlobalStatusBarManager for state management via Riverpod - Add StatusValidator with initial validation for GET requests with bodies Signed-off-by: Balasubramaniam12007 --- lib/providers/providers.dart | 1 + lib/providers/status_message_provider.dart | 37 ++++++++++++ .../home_page/editor_pane/editor_pane.dart | 21 ++++--- .../editor_pane/global_status_bar.dart | 60 +++++++++++++++++++ lib/utils/status_validator.dart | 20 +++++++ 5 files changed, 131 insertions(+), 8 deletions(-) create mode 100644 lib/providers/status_message_provider.dart create mode 100644 lib/screens/home_page/editor_pane/global_status_bar.dart create mode 100644 lib/utils/status_validator.dart diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart index 29fc6e595..438226eba 100644 --- a/lib/providers/providers.dart +++ b/lib/providers/providers.dart @@ -3,3 +3,4 @@ export 'environment_providers.dart'; export 'history_providers.dart'; export 'settings_providers.dart'; export 'ui_providers.dart'; +export 'status_message_provider.dart'; \ No newline at end of file diff --git a/lib/providers/status_message_provider.dart b/lib/providers/status_message_provider.dart new file mode 100644 index 000000000..aeed7fc9b --- /dev/null +++ b/lib/providers/status_message_provider.dart @@ -0,0 +1,37 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash/providers/collection_providers.dart'; +import 'package:apidash/utils/status_validator.dart'; + +enum StatusMessageType { defaultType, info, warning, error } + +class StatusMessage { + final String message; + final StatusMessageType type; + + StatusMessage(this.message, this.type); +} + +final statusMessageProvider = + StateNotifierProvider((ref) { + return GlobalStatusBarManager(ref); +}); + +class GlobalStatusBarManager extends StateNotifier { + final Ref ref; + + GlobalStatusBarManager(this.ref) + : super(StatusMessage("Global Status Bar", StatusMessageType.defaultType)) { + // Listen for request changes and validate + ref.listen(selectedRequestModelProvider, (previous, next) { + final method = next?.httpRequestModel?.method ?? HTTPVerb.get; + final body = next?.httpRequestModel?.body; + _updateStatusMessage(StatusValidator().validateRequest(method, body)); + }); + } + + // Updates the status message + void _updateStatusMessage(StatusMessage newMessage) { + state = newMessage; + } +} diff --git a/lib/screens/home_page/editor_pane/editor_pane.dart b/lib/screens/home_page/editor_pane/editor_pane.dart index 7ff67f5c1..36da4bd03 100644 --- a/lib/screens/home_page/editor_pane/editor_pane.dart +++ b/lib/screens/home_page/editor_pane/editor_pane.dart @@ -3,19 +3,24 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; import 'editor_default.dart'; import 'editor_request.dart'; +import 'global_status_bar.dart'; class RequestEditorPane extends ConsumerWidget { - const RequestEditorPane({ - super.key, - }); + const RequestEditorPane({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final selectedId = ref.watch(selectedIdStateProvider); - if (selectedId == null) { - return const RequestEditorDefault(); - } else { - return const RequestEditor(); - } + + return Column( + children: [ + Expanded( + child: selectedId == null + ? const RequestEditorDefault() + : const RequestEditor(), + ), + const GlobalStatusBar(), + ], + ); } } diff --git a/lib/screens/home_page/editor_pane/global_status_bar.dart b/lib/screens/home_page/editor_pane/global_status_bar.dart new file mode 100644 index 000000000..9cb3a3e1b --- /dev/null +++ b/lib/screens/home_page/editor_pane/global_status_bar.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; + +class GlobalStatusBar extends ConsumerWidget { + const GlobalStatusBar({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final statusMessage = ref.watch(statusMessageProvider); + + // Get color based on message type + final color = statusMessage.type == StatusMessageType.info + ? kColorSchemeSeed + : statusMessage.type == StatusMessageType.warning + ? kColorHttpMethodPut + : statusMessage.type == StatusMessageType.error + ? kColorDarkDanger + : kColorBlack; + + // Get icon based on message type + final icon = statusMessage.type == StatusMessageType.error + ? Icons.error_outline + : statusMessage.type == StatusMessageType.warning + ? Icons.warning_amber_outlined + : statusMessage.type == StatusMessageType.info + ? Icons.info_outline + : null; + + return Container( + padding: kPh12, + width: double.infinity, + height: 40, + // Use background color only for status messages (info/warning/error) + color: icon != null ? color.withOpacity(kForegroundOpacity) : kColorWhite, + child: Row( + children: [ + // Only display icons for info, warning, and error states + if (icon != null) ...[ + Icon(icon, size: kButtonIconSizeSmall, color: color), + kHSpacer8, + ], + Expanded( + child: Text( + statusMessage.message, + style: TextStyle( + color: color, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + // Prevent text overflow issues + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + } +} diff --git a/lib/utils/status_validator.dart b/lib/utils/status_validator.dart new file mode 100644 index 000000000..75f91b838 --- /dev/null +++ b/lib/utils/status_validator.dart @@ -0,0 +1,20 @@ +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash/providers/status_message_provider.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; + +class StatusValidator { + StatusMessage validateRequest(HTTPVerb method, String? body) { + if (_isInvalidGetRequest(method, body)) { + return StatusMessage( + "GET requests cannot have a body. Remove the body or change the method to POST.", + StatusMessageType.info, + ); + } + return StatusMessage("Global Status Bar", StatusMessageType.defaultType); + } + + bool _isInvalidGetRequest(HTTPVerb method, String? body) { + return method == HTTPVerb.get && body != null && body.isNotEmpty; + } + +} From 561f1094a312c7ab2a3d22d1f0c3898a87319ec3 Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Mon, 28 Apr 2025 01:28:06 +0530 Subject: [PATCH 2/3] Enhance GlobalStatusBar with performance optimizations and features - Add JSON validation to StatusValidator for ContentType.json requests - Implement expandable multi-line messages in GlobalStatusBar - Optimize GlobalStatusBar with memoization and Riverpod select - Prevent redundant state updates in GlobalStatusBarManager - Update GET request validation to use warning type - Supports both dark and light mode Signed-off-by: Balasubramaniam12007 --- lib/consts.dart | 5 + lib/providers/status_message_provider.dart | 31 +++-- .../editor_pane/global_status_bar.dart | 124 ++++++++++++------ lib/utils/status_validator.dart | 44 ++++++- 4 files changed, 147 insertions(+), 57 deletions(-) diff --git a/lib/consts.dart b/lib/consts.dart index 628598e47..d3eadc256 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -485,3 +485,8 @@ const kMsgClearHistory = const kMsgClearHistorySuccess = 'History cleared successfully'; const kMsgClearHistoryError = 'Error clearing history'; const kMsgShareError = "Unable to share"; + +const kStatusBarHeight = 40.0; +const kStatusBarFontSize = 14.0; +const kStatusBarDefaultMessage = "GLOBAL STATUS BAR"; + diff --git a/lib/providers/status_message_provider.dart b/lib/providers/status_message_provider.dart index aeed7fc9b..5b7ed44c5 100644 --- a/lib/providers/status_message_provider.dart +++ b/lib/providers/status_message_provider.dart @@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash/providers/collection_providers.dart'; import 'package:apidash/utils/status_validator.dart'; +import 'package:apidash/consts.dart'; enum StatusMessageType { defaultType, info, warning, error } @@ -19,19 +20,33 @@ final statusMessageProvider = class GlobalStatusBarManager extends StateNotifier { final Ref ref; + final StatusValidator _validator = StatusValidator(); GlobalStatusBarManager(this.ref) - : super(StatusMessage("Global Status Bar", StatusMessageType.defaultType)) { - // Listen for request changes and validate + : super(StatusMessage(kStatusBarDefaultMessage, StatusMessageType.defaultType)) { ref.listen(selectedRequestModelProvider, (previous, next) { - final method = next?.httpRequestModel?.method ?? HTTPVerb.get; - final body = next?.httpRequestModel?.body; - _updateStatusMessage(StatusValidator().validateRequest(method, body)); + if (next?.httpRequestModel != null) { + final httpModel = next!.httpRequestModel!; + final method = httpModel.method; + final body = httpModel.body; + final contentType = httpModel.bodyContentType; + + final newMessage = _validator.validateRequest(method, body, contentType: contentType); + // Only update if the new message is different + if (newMessage.message != state.message || newMessage.type != state.type) { + _updateStatusMessage(newMessage); + } + } else { + _resetStatusMessage(); + } }); } - // Updates the status message - void _updateStatusMessage(StatusMessage newMessage) { + void _updateStatusMessage(StatusMessage newMessage) { // Updates the status message state = newMessage; } -} + + void _resetStatusMessage() { + state = StatusMessage(kStatusBarDefaultMessage, StatusMessageType.defaultType); + } +} \ No newline at end of file diff --git a/lib/screens/home_page/editor_pane/global_status_bar.dart b/lib/screens/home_page/editor_pane/global_status_bar.dart index 9cb3a3e1b..2684525e5 100644 --- a/lib/screens/home_page/editor_pane/global_status_bar.dart +++ b/lib/screens/home_page/editor_pane/global_status_bar.dart @@ -2,59 +2,99 @@ import 'package:flutter/material.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; +import 'package:apidash/consts.dart'; -class GlobalStatusBar extends ConsumerWidget { +class GlobalStatusBar extends ConsumerStatefulWidget { const GlobalStatusBar({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { - final statusMessage = ref.watch(statusMessageProvider); - - // Get color based on message type - final color = statusMessage.type == StatusMessageType.info - ? kColorSchemeSeed - : statusMessage.type == StatusMessageType.warning - ? kColorHttpMethodPut - : statusMessage.type == StatusMessageType.error - ? kColorDarkDanger - : kColorBlack; - - // Get icon based on message type - final icon = statusMessage.type == StatusMessageType.error - ? Icons.error_outline - : statusMessage.type == StatusMessageType.warning - ? Icons.warning_amber_outlined - : statusMessage.type == StatusMessageType.info - ? Icons.info_outline - : null; + ConsumerState createState() => _GlobalStatusBarState(); +} + +class _GlobalStatusBarState extends ConsumerState { + bool _isExpanded = false; + List _cachedLines = []; + String _lastMessage = ''; + + @override + Widget build(BuildContext context) { + final message = ref.watch(statusMessageProvider.select((s) => s.message)); + final type = ref.watch(statusMessageProvider.select((s) => s.type)); + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + _cachedLines = message != _lastMessage ? message.split('\n') : _cachedLines; + _lastMessage = message; + final needsExpansion = _cachedLines.length > 1; + + final color = switch (type) { + StatusMessageType.info => kColorSchemeSeed, + StatusMessageType.warning => kColorHttpMethodPut, + StatusMessageType.error => kColorDarkDanger, + _ => isDarkMode ? Colors.white : kColorBlack, + }; + + final icon = switch (type) { + StatusMessageType.error => Icons.error_outline, + StatusMessageType.warning => Icons.warning_amber_outlined, + StatusMessageType.info => Icons.info_outline, + _ => null, + }; return Container( - padding: kPh12, width: double.infinity, - height: 40, - // Use background color only for status messages (info/warning/error) - color: icon != null ? color.withOpacity(kForegroundOpacity) : kColorWhite, - child: Row( + color: icon != null + ? color.withOpacity(kForegroundOpacity) + : isDarkMode + ? Theme.of(context).colorScheme.surface + : kColorWhite, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - // Only display icons for info, warning, and error states - if (icon != null) ...[ - Icon(icon, size: kButtonIconSizeSmall, color: color), - kHSpacer8, - ], - Expanded( - child: Text( - statusMessage.message, - style: TextStyle( - color: color, - fontSize: 14, - fontWeight: FontWeight.w500, - ), - // Prevent text overflow issues - overflow: TextOverflow.ellipsis, + Container( + padding: kPh12, + height: kStatusBarHeight, + child: Row( + children: [ + if (icon != null) ...[ + Icon(icon, size: kButtonIconSizeSmall, color: color), + kHSpacer8, + ], + Expanded( + child: Text( + _cachedLines.isNotEmpty ? _cachedLines.first : '', + style: TextStyle(color: color, fontSize: 14, fontWeight: FontWeight.w500), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + if (needsExpansion) + InkWell( + onTap: () => setState(() => _isExpanded = !_isExpanded), + customBorder: const CircleBorder(), + child: Icon( + _isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, + size: 24, + color: color, + ), + ), + ], ), ), + if (_isExpanded && needsExpansion) + Container( + width: double.infinity, + padding: EdgeInsets.only( + left: 12 + (icon != null ? 32 : 0), + right: 12, + bottom: 8, + ), + child: Text( + _cachedLines.skip(1).join('\n'), + style: TextStyle(color: color, fontSize: 14, fontWeight: FontWeight.w500), + ), + ), ], ), ); } -} +} \ No newline at end of file diff --git a/lib/utils/status_validator.dart b/lib/utils/status_validator.dart index 75f91b838..2ac8d9c57 100644 --- a/lib/utils/status_validator.dart +++ b/lib/utils/status_validator.dart @@ -1,20 +1,50 @@ import 'package:apidash_core/apidash_core.dart'; -import 'package:apidash/providers/status_message_provider.dart'; -import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/consts.dart'; +import 'dart:convert'; class StatusValidator { - StatusMessage validateRequest(HTTPVerb method, String? body) { + StatusMessage validateRequest(HTTPVerb method, String? body, {ContentType? contentType}) { + // Check for GET requests with body if (_isInvalidGetRequest(method, body)) { return StatusMessage( "GET requests cannot have a body. Remove the body or change the method to POST.", - StatusMessageType.info, + StatusMessageType.warning, ); } - return StatusMessage("Global Status Bar", StatusMessageType.defaultType); + + //simple check for JSON validation for testing + if (contentType == ContentType.json && body != null && body.isNotEmpty) { + final jsonValidation = _validateJson(body); + if (jsonValidation != null) { + return jsonValidation; + } + } + + return StatusMessage(kStatusBarDefaultMessage, StatusMessageType.defaultType); } bool _isInvalidGetRequest(HTTPVerb method, String? body) { return method == HTTPVerb.get && body != null && body.isNotEmpty; } - -} + + StatusMessage? _validateJson(String jsonText) { + if (jsonText.trim().isEmpty) return null; + + try { + json.decode(jsonText); + return null; // Valid JSON + } catch (e) { + // Extract the error message + final errorMsg = e.toString(); + final simplifiedError = errorMsg.contains('FormatException') + ? 'Invalid JSON: ${errorMsg.split('FormatException: ').last}' + : 'Invalid JSON format'; + + return StatusMessage( + simplifiedError, + StatusMessageType.error, + ); + } + } +} \ No newline at end of file From 9043c966adf23840f51e228ed907f3a04addb369 Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Mon, 28 Apr 2025 01:54:01 +0530 Subject: [PATCH 3/3] Enhance the code with constants. --- lib/consts.dart | 13 +++++++++++-- .../home_page/editor_pane/global_status_bar.dart | 15 +++++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/lib/consts.dart b/lib/consts.dart index d3eadc256..a6047abae 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -486,7 +486,16 @@ const kMsgClearHistorySuccess = 'History cleared successfully'; const kMsgClearHistoryError = 'Error clearing history'; const kMsgShareError = "Unable to share"; +// Status Bar Constants const kStatusBarHeight = 40.0; const kStatusBarFontSize = 14.0; -const kStatusBarDefaultMessage = "GLOBAL STATUS BAR"; - +const kStatusBarDefaultMessage = "Global Status Bar"; + +const kStatusBarExpandIconSize = 24.0; +const kStatusBarExpandedPadding = EdgeInsets.only(left: 12.0, right: 12.0, bottom: 8.0); +const kStatusBarIconPaddingOffset = 32.0; +const kStatusBarFontWeight = FontWeight.w500; +const kStatusBarTextStyle = TextStyle( + fontSize: kStatusBarFontSize, + fontWeight: kStatusBarFontWeight, +); diff --git a/lib/screens/home_page/editor_pane/global_status_bar.dart b/lib/screens/home_page/editor_pane/global_status_bar.dart index 2684525e5..b3ccac215 100644 --- a/lib/screens/home_page/editor_pane/global_status_bar.dart +++ b/lib/screens/home_page/editor_pane/global_status_bar.dart @@ -30,7 +30,7 @@ class _GlobalStatusBarState extends ConsumerState { StatusMessageType.info => kColorSchemeSeed, StatusMessageType.warning => kColorHttpMethodPut, StatusMessageType.error => kColorDarkDanger, - _ => isDarkMode ? Colors.white : kColorBlack, + _ => isDarkMode ? kColorWhite : kColorBlack, }; final icon = switch (type) { @@ -62,7 +62,7 @@ class _GlobalStatusBarState extends ConsumerState { Expanded( child: Text( _cachedLines.isNotEmpty ? _cachedLines.first : '', - style: TextStyle(color: color, fontSize: 14, fontWeight: FontWeight.w500), + style: kStatusBarTextStyle.copyWith(color: color), overflow: TextOverflow.ellipsis, maxLines: 1, ), @@ -73,7 +73,7 @@ class _GlobalStatusBarState extends ConsumerState { customBorder: const CircleBorder(), child: Icon( _isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, - size: 24, + size: kStatusBarExpandIconSize, color: color, ), ), @@ -83,14 +83,13 @@ class _GlobalStatusBarState extends ConsumerState { if (_isExpanded && needsExpansion) Container( width: double.infinity, - padding: EdgeInsets.only( - left: 12 + (icon != null ? 32 : 0), - right: 12, - bottom: 8, + padding: kStatusBarExpandedPadding.copyWith( + left: kStatusBarExpandedPadding.left + + (icon != null ? kStatusBarIconPaddingOffset : 0), ), child: Text( _cachedLines.skip(1).join('\n'), - style: TextStyle(color: color, fontSize: 14, fontWeight: FontWeight.w500), + style: kStatusBarTextStyle.copyWith(color: color), ), ), ],