Skip to content

feat(status): Implement global status bar and request body in get val… #665

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions lib/consts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -485,3 +485,17 @@ const kMsgClearHistory =
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 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,
);
1 change: 1 addition & 0 deletions lib/providers/providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
52 changes: 52 additions & 0 deletions lib/providers/status_message_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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 }

class StatusMessage {
final String message;
final StatusMessageType type;

StatusMessage(this.message, this.type);
}

final statusMessageProvider =
StateNotifierProvider<GlobalStatusBarManager, StatusMessage>((ref) {
return GlobalStatusBarManager(ref);
});

class GlobalStatusBarManager extends StateNotifier<StatusMessage> {
final Ref ref;
final StatusValidator _validator = StatusValidator();

GlobalStatusBarManager(this.ref)
: super(StatusMessage(kStatusBarDefaultMessage, StatusMessageType.defaultType)) {
ref.listen(selectedRequestModelProvider, (previous, next) {
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();
}
});
}

void _updateStatusMessage(StatusMessage newMessage) { // Updates the status message
state = newMessage;
}

void _resetStatusMessage() {
state = StatusMessage(kStatusBarDefaultMessage, StatusMessageType.defaultType);
}
}
21 changes: 13 additions & 8 deletions lib/screens/home_page/editor_pane/editor_pane.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
],
);
}
}
99 changes: 99 additions & 0 deletions lib/screens/home_page/editor_pane/global_status_bar.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,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 ConsumerStatefulWidget {
const GlobalStatusBar({super.key});

@override
ConsumerState<GlobalStatusBar> createState() => _GlobalStatusBarState();
}

class _GlobalStatusBarState extends ConsumerState<GlobalStatusBar> {
bool _isExpanded = false;
List<String> _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 ? kColorWhite : kColorBlack,
};

final icon = switch (type) {
StatusMessageType.error => Icons.error_outline,
StatusMessageType.warning => Icons.warning_amber_outlined,
StatusMessageType.info => Icons.info_outline,
_ => null,
};

return Container(
width: double.infinity,
color: icon != null
? color.withOpacity(kForegroundOpacity)
: isDarkMode
? Theme.of(context).colorScheme.surface
: kColorWhite,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
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: kStatusBarTextStyle.copyWith(color: color),
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: kStatusBarExpandIconSize,
color: color,
),
),
],
),
),
if (_isExpanded && needsExpansion)
Container(
width: double.infinity,
padding: kStatusBarExpandedPadding.copyWith(
left: kStatusBarExpandedPadding.left +
(icon != null ? kStatusBarIconPaddingOffset : 0),
),
child: Text(
_cachedLines.skip(1).join('\n'),
style: kStatusBarTextStyle.copyWith(color: color),
),
),
],
),
);
}
}
50 changes: 50 additions & 0 deletions lib/utils/status_validator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import 'package:apidash_core/apidash_core.dart';
import 'package:apidash/providers/providers.dart';
import 'package:apidash/consts.dart';
import 'dart:convert';

class StatusValidator {
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.warning,
);
}

//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,
);
}
}
}