Merge branch 'foss42:main' into har_importer

This commit is contained in:
Mohammed Ayaan
2025-03-30 07:12:59 +05:30
committed by GitHub
29 changed files with 580 additions and 279 deletions

View File

@@ -84,7 +84,7 @@ class ApidashTestRequestHelper {
var headerCells = find.descendant(
of: find.byType(EditRequestHeaders),
matching: find.byType(HeaderField));
matching: find.byType(EnvHeaderField));
var valueCells = find.descendant(
of: find.byType(EditRequestHeaders),
matching: find.byType(EnvCellField));
@@ -95,7 +95,7 @@ class ApidashTestRequestHelper {
tester.testTextInput.enterText(keyValuePairs[i].$2);
headerCells = find.descendant(
of: find.byType(EditRequestHeaders),
matching: find.byType(HeaderField));
matching: find.byType(EnvHeaderField));
valueCells = find.descendant(
of: find.byType(EditRequestHeaders),
matching: find.byType(EnvCellField));

View File

@@ -1,15 +1,18 @@
export 'api_type_dropdown.dart';
export 'button_navbar.dart';
export 'code_pane.dart';
export 'editor_title.dart';
export 'editor_title_actions.dart';
export 'envfield_url.dart';
export 'editor_title.dart';
export 'env_regexp_span_builder.dart';
export 'env_trigger_field.dart';
export 'env_trigger_options.dart';
export 'envfield_cell.dart';
export 'envfield_header.dart';
export 'envfield_url.dart';
export 'environment_dropdown.dart';
export 'envvar_indicator.dart';
export 'envvar_span.dart';
export 'envvar_popover.dart';
export 'env_trigger_options.dart';
export 'envvar_span.dart';
export 'sidebar_filter.dart';
export 'sidebar_header.dart';
export 'sidebar_save_button.dart';

View File

@@ -9,20 +9,29 @@ class EnvironmentTriggerField extends StatefulWidget {
super.key,
required this.keyId,
this.initialValue,
this.controller,
this.focusNode,
this.onChanged,
this.onFieldSubmitted,
this.style,
this.decoration,
this.optionsWidthFactor,
});
this.autocompleteNoTrigger,
}) : assert(
!(controller != null && initialValue != null),
'controller and initialValue cannot be simultaneously defined.',
);
final String keyId;
final String? initialValue;
final TextEditingController? controller;
final FocusNode? focusNode;
final void Function(String)? onChanged;
final void Function(String)? onFieldSubmitted;
final TextStyle? style;
final InputDecoration? decoration;
final double? optionsWidthFactor;
final AutocompleteNoTrigger? autocompleteNoTrigger;
@override
State<EnvironmentTriggerField> createState() =>
@@ -30,21 +39,24 @@ class EnvironmentTriggerField extends StatefulWidget {
}
class EnvironmentTriggerFieldState extends State<EnvironmentTriggerField> {
final TextEditingController controller = TextEditingController();
final FocusNode focusNode = FocusNode();
late TextEditingController controller;
late FocusNode _focusNode;
@override
void initState() {
super.initState();
controller.text = widget.initialValue ?? '';
controller.selection =
TextSelection.collapsed(offset: controller.text.length);
controller = widget.controller ??
TextEditingController.fromValue(TextEditingValue(
text: widget.initialValue!,
selection:
TextSelection.collapsed(offset: widget.initialValue!.length)));
_focusNode = widget.focusNode ?? FocusNode();
}
@override
void dispose() {
controller.dispose();
focusNode.dispose();
_focusNode.dispose();
super.dispose();
}
@@ -53,9 +65,11 @@ class EnvironmentTriggerFieldState extends State<EnvironmentTriggerField> {
super.didUpdateWidget(oldWidget);
if ((oldWidget.keyId != widget.keyId) ||
(oldWidget.initialValue != widget.initialValue)) {
controller.text = widget.initialValue ?? "";
controller.selection =
TextSelection.collapsed(offset: controller.text.length);
controller = widget.controller ??
TextEditingController.fromValue(TextEditingValue(
text: widget.initialValue!,
selection: TextSelection.collapsed(
offset: widget.initialValue!.length)));
}
}
@@ -64,9 +78,10 @@ class EnvironmentTriggerFieldState extends State<EnvironmentTriggerField> {
return MultiTriggerAutocomplete(
key: Key(widget.keyId),
textEditingController: controller,
focusNode: focusNode,
optionsWidthFactor: widget.optionsWidthFactor,
focusNode: _focusNode,
optionsWidthFactor: widget.optionsWidthFactor ?? 1,
autocompleteTriggers: [
if (widget.autocompleteNoTrigger != null) widget.autocompleteNoTrigger!,
AutocompleteTrigger(
trigger: '{',
triggerEnd: "}}",
@@ -108,7 +123,7 @@ class EnvironmentTriggerFieldState extends State<EnvironmentTriggerField> {
onSubmitted: widget.onFieldSubmitted,
specialTextSpanBuilder: EnvRegExpSpanBuilder(),
onTapOutside: (event) {
focusNode.unfocus();
_focusNode.unfocus();
},
);
},

View File

@@ -1,11 +1,9 @@
import 'package:apidash/consts.dart';
import 'package:apidash_core/apidash_core.dart';
import 'package:apidash_design_system/apidash_design_system.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:apidash/providers/providers.dart';
import 'package:apidash_core/models/models.dart';
import 'package:apidash/utils/utils.dart';
import 'envvar_indicator.dart';
class EnvironmentTriggerOptions extends ConsumerWidget {
@@ -26,41 +24,21 @@ class EnvironmentTriggerOptions extends ConsumerWidget {
getEnvironmentTriggerSuggestions(query, envMap, activeEnvironmentId);
return suggestions == null || suggestions.isEmpty
? const SizedBox.shrink()
: ClipRRect(
borderRadius: kBorderRadius8,
child: Material(
type: MaterialType.card,
elevation: 8,
child: ConstrainedBox(
constraints:
const BoxConstraints(maxHeight: kSuggestionsMenuMaxHeight),
child: Ink(
width: kSuggestionsMenuWidth,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: kBorderRadius8,
border: Border.all(
color: Theme.of(context).colorScheme.outlineVariant,
),
),
child: ListView.separated(
shrinkWrap: true,
itemCount: suggestions.length,
separatorBuilder: (context, index) =>
const Divider(height: 2),
itemBuilder: (context, index) {
final suggestion = suggestions[index];
return ListTile(
dense: true,
leading: EnvVarIndicator(suggestion: suggestion),
title: Text(suggestion.variable.key),
subtitle: Text(suggestion.variable.value),
onTap: () => onSuggestionTap(suggestion),
);
},
),
),
),
: SuggestionsMenuBox(
child: ListView.separated(
shrinkWrap: true,
itemCount: suggestions.length,
separatorBuilder: (context, index) => const Divider(height: 2),
itemBuilder: (context, index) {
final suggestion = suggestions[index];
return ListTile(
dense: true,
leading: EnvVarIndicator(suggestion: suggestion),
title: Text(suggestion.variable.key),
subtitle: Text(suggestion.variable.value),
onTap: () => onSuggestionTap(suggestion),
);
},
),
);
}

View File

@@ -1,5 +1,6 @@
import 'package:apidash_design_system/apidash_design_system.dart';
import 'package:flutter/material.dart';
import 'package:multi_trigger_autocomplete_plus/multi_trigger_autocomplete_plus.dart';
import 'env_trigger_field.dart';
class EnvCellField extends StatelessWidget {
@@ -10,6 +11,8 @@ class EnvCellField extends StatelessWidget {
this.hintText,
this.onChanged,
this.colorScheme,
this.autocompleteNoTrigger,
this.focusNode,
});
final String keyId;
@@ -17,6 +20,8 @@ class EnvCellField extends StatelessWidget {
final String? hintText;
final void Function(String)? onChanged;
final ColorScheme? colorScheme;
final AutocompleteNoTrigger? autocompleteNoTrigger;
final FocusNode? focusNode;
@override
Widget build(BuildContext context) {
@@ -24,13 +29,16 @@ class EnvCellField extends StatelessWidget {
return EnvironmentTriggerField(
keyId: keyId,
initialValue: initialValue,
focusNode: focusNode,
style: kCodeStyle.copyWith(
color: clrScheme.onSurface,
fontSize: Theme.of(context).textTheme.bodyMedium?.fontSize,
),
decoration: getTextFieldInputDecoration(
clrScheme,
hintText: hintText,
),
autocompleteNoTrigger: autocompleteNoTrigger,
onChanged: onChanged,
);
}

View File

@@ -0,0 +1,61 @@
import 'package:apidash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:multi_trigger_autocomplete_plus/multi_trigger_autocomplete_plus.dart';
import 'package:apidash/utils/utils.dart';
import 'envfield_cell.dart';
class EnvHeaderField extends StatefulWidget {
const EnvHeaderField({
super.key,
required this.keyId,
this.hintText,
this.initialValue,
this.onChanged,
this.colorScheme,
});
final String keyId;
final String? hintText;
final String? initialValue;
final void Function(String)? onChanged;
final ColorScheme? colorScheme;
@override
State<EnvHeaderField> createState() => _EnvHeaderFieldState();
}
class _EnvHeaderFieldState extends State<EnvHeaderField> {
final FocusNode focusNode = FocusNode();
@override
Widget build(BuildContext context) {
var colorScheme = widget.colorScheme ?? Theme.of(context).colorScheme;
return EnvCellField(
keyId: widget.keyId,
hintText: widget.hintText,
initialValue: widget.initialValue ?? "",
focusNode: focusNode,
onChanged: widget.onChanged,
colorScheme: colorScheme,
autocompleteNoTrigger: AutocompleteNoTrigger(
optionsViewBuilder: (context, autocompleteQuery, controller) {
return HeaderSuggestions(
suggestionsCallback: headerSuggestionCallback,
query: autocompleteQuery.query,
onSuggestionTap: (suggestion) {
controller.text = suggestion;
widget.onChanged?.call(controller.text);
focusNode.unfocus();
});
}),
);
}
Future<List<String>?> headerSuggestionCallback(String pattern) async {
if (pattern.isEmpty) {
return null;
}
return getHeaderSuggestions(pattern)
.where(
(suggestion) => suggestion.toLowerCase() != pattern.toLowerCase())
.toList();
}
}

View File

@@ -10,18 +10,21 @@ class EnvURLField extends StatelessWidget {
this.initialValue,
this.onChanged,
this.onFieldSubmitted,
this.focusNode,
});
final String selectedId;
final String? initialValue;
final void Function(String)? onChanged;
final void Function(String)? onFieldSubmitted;
final FocusNode? focusNode;
@override
Widget build(BuildContext context) {
return EnvironmentTriggerField(
keyId: "url-$selectedId",
initialValue: initialValue,
focusNode: focusNode,
style: kCodeStyle,
decoration: InputDecoration(
hintText: kHintTextUrlCard,

View File

@@ -140,7 +140,7 @@ class HisRequestBody extends ConsumerWidget {
// TODO: Fix JsonTextFieldEditor & plug it here
ContentType.json => Padding(
padding: kPt5o10,
child: TextFieldEditor(
child: JsonTextFieldEditor(
key: Key("${selectedHistoryModel?.historyId}-json-body"),
fieldKey:
"${selectedHistoryModel?.historyId}-json-body-viewer",

View File

@@ -20,6 +20,9 @@ class EditRequestBody extends ConsumerWidget {
.select((value) => value?.httpRequestModel?.bodyContentType));
final apiType = ref
.watch(selectedRequestModelProvider.select((value) => value?.apiType));
final mode = ref.watch(settingsProvider.select(
(value) => value.isDark,
));
return Column(
children: [
@@ -42,12 +45,11 @@ class EditRequestBody extends ConsumerWidget {
child: switch (contentType) {
ContentType.formdata =>
const Padding(padding: kPh4, child: FormDataWidget()),
// TODO: Fix JsonTextFieldEditor & plug it here
ContentType.json => Padding(
padding: kPt5o10,
child: TextFieldEditor(
child: JsonTextFieldEditor(
key: Key("$selectedId-json-body"),
fieldKey: "$selectedId-json-body-editor",
fieldKey: "$selectedId-json-body-editor-$mode",
initialValue: requestModel?.httpRequestModel?.body,
onChanged: (String value) {
ref

View File

@@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:data_table_2/data_table_2.dart';
import 'package:apidash/providers/providers.dart';
import 'package:apidash/widgets/widgets.dart';
import 'package:apidash/consts.dart';
import 'package:apidash/screens/common_widgets/common_widgets.dart';
@@ -104,7 +103,7 @@ class EditRequestHeadersState extends ConsumerState<EditRequestHeaders> {
),
),
DataCell(
HeaderField(
EnvHeaderField(
keyId: "$selectedId-$index-headers-k-$seed",
initialValue: headerRows[index].name,
hintText: kHintAddName,

View File

@@ -40,21 +40,21 @@ class SettingsPage extends ConsumerWidget {
child: ListView(
shrinkWrap: true,
children: [
SwitchListTile(
hoverColor: kColorTransparent,
title: const Text('Switch Theme Mode'),
subtitle: Text(
'Current selection: ${settings.isDark ? "Dark Mode" : "Light mode"}'),
ADListTile(
type: ListTileType.switchOnOff,
title: 'Switch Theme Mode',
subtitle:
'Current selection: ${settings.isDark ? "Dark Mode" : "Light mode"}',
value: settings.isDark,
onChanged: (bool? value) {
ref.read(settingsProvider.notifier).update(isDark: value);
},
),
SwitchListTile(
hoverColor: kColorTransparent,
title: const Text('Collection Pane Scrollbar Visiblity'),
subtitle: Text(
'Current selection: ${settings.alwaysShowCollectionPaneScrollbar ? "Always show" : "Show only when scrolling"}'),
ADListTile(
type: ListTileType.switchOnOff,
title: 'Collection Pane Scrollbar Visiblity',
subtitle:
'Current selection: ${settings.alwaysShowCollectionPaneScrollbar ? "Always show" : "Show only when scrolling"}',
value: settings.alwaysShowCollectionPaneScrollbar,
onChanged: (bool? value) {
ref
@@ -77,12 +77,11 @@ class SettingsPage extends ConsumerWidget {
),
),
!kIsWeb
? SwitchListTile(
hoverColor: kColorTransparent,
title: const Text('Disable SSL verification'),
subtitle: Text(
'Current selection: ${settings.isSSLDisabled ? "SSL Verification Disabled" : "SSL Verification Enabled"}',
),
? ADListTile(
type: ListTileType.switchOnOff,
title: 'Disable SSL verification',
subtitle:
'Current selection: ${settings.isSSLDisabled ? "SSL Verification Disabled" : "SSL Verification Enabled"}',
value: settings.isSSLDisabled,
onChanged: (bool? value) {
ref

View File

@@ -102,7 +102,7 @@ class _TextFieldEditorState extends State<TextFieldEditor> {
),
filled: true,
hoverColor: kColorTransparent,
fillColor: Theme.of(context).colorScheme.surfaceContainerLow,
fillColor: Theme.of(context).colorScheme.surfaceContainerLowest,
),
),
);

View File

@@ -1,9 +1,9 @@
import 'dart:math' as math;
import 'package:apidash/consts.dart';
import 'package:apidash_design_system/apidash_design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:json_text_field/json_text_field.dart';
import 'package:apidash/consts.dart';
class JsonTextFieldEditor extends StatefulWidget {
const JsonTextFieldEditor({
@@ -11,11 +11,15 @@ class JsonTextFieldEditor extends StatefulWidget {
required this.fieldKey,
this.onChanged,
this.initialValue,
this.hintText,
this.readOnly = false,
});
final String fieldKey;
final Function(String)? onChanged;
final String? initialValue;
final String? hintText;
final bool readOnly;
@override
State<JsonTextFieldEditor> createState() => _JsonTextFieldEditorState();
}
@@ -44,6 +48,9 @@ class _JsonTextFieldEditorState extends State<JsonTextFieldEditor> {
@override
void initState() {
super.initState();
if (widget.initialValue != null) {
controller.text = widget.initialValue!;
}
controller.formatJson(sortJson: false);
editorFocusNode = FocusNode(debugLabel: "Editor Focus Node");
}
@@ -55,75 +62,127 @@ class _JsonTextFieldEditorState extends State<JsonTextFieldEditor> {
}
@override
Widget build(BuildContext context) {
if (widget.initialValue != null) {
controller.text = widget.initialValue!;
void didUpdateWidget(JsonTextFieldEditor oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.initialValue != widget.initialValue) {
controller.text = widget.initialValue ?? "";
controller.selection =
TextSelection.collapsed(offset: controller.text.length);
}
return CallbackShortcuts(
bindings: <ShortcutActivator, VoidCallback>{
const SingleActivator(LogicalKeyboardKey.tab): () {
insertTab();
},
},
child: JsonTextField(
stringHighlightStyle: kCodeStyle.copyWith(
color: Theme.of(context).colorScheme.secondary,
),
keyHighlightStyle: kCodeStyle.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
errorContainerDecoration: BoxDecoration(
color: Theme.of(context).colorScheme.error.withOpacity(
kForegroundOpacity,
if (oldWidget.fieldKey != widget.fieldKey) {
// TODO: JsonTextField uses ExtendedTextField which does
// not rebuild because no key is provided
// so light mode to dark mode switching leads to incorrect color.
setState(() {});
}
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
CallbackShortcuts(
bindings: <ShortcutActivator, VoidCallback>{
const SingleActivator(LogicalKeyboardKey.tab): () {
insertTab();
},
},
child: JsonTextField(
key: Key(widget.fieldKey),
commonTextStyle: kCodeStyle.copyWith(
color: Theme.of(context).brightness == Brightness.dark
? kDarkCodeTheme['root']?.color
: kLightCodeTheme['root']?.color,
),
specialCharHighlightStyle: kCodeStyle.copyWith(
color: Theme.of(context).brightness == Brightness.dark
? kDarkCodeTheme['root']?.color
: kLightCodeTheme['root']?.color,
),
stringHighlightStyle: kCodeStyle.copyWith(
color: Theme.of(context).brightness == Brightness.dark
? kDarkCodeTheme['string']?.color
: kLightCodeTheme['string']?.color,
),
numberHighlightStyle: kCodeStyle.copyWith(
color: Theme.of(context).brightness == Brightness.dark
? kDarkCodeTheme['number']?.color
: kLightCodeTheme['number']?.color,
),
boolHighlightStyle: kCodeStyle.copyWith(
color: Theme.of(context).brightness == Brightness.dark
? kDarkCodeTheme['literal']?.color
: kLightCodeTheme['literal']?.color,
),
nullHighlightStyle: kCodeStyle.copyWith(
color: Theme.of(context).brightness == Brightness.dark
? kDarkCodeTheme['variable']?.color
: kLightCodeTheme['variable']?.color,
),
keyHighlightStyle: kCodeStyle.copyWith(
color: Theme.of(context).brightness == Brightness.dark
? kDarkCodeTheme['attr']?.color
: kLightCodeTheme['attr']?.color,
fontWeight: FontWeight.bold,
),
// errorContainerDecoration: BoxDecoration(
// color: Theme.of(context).colorScheme.error.withOpacity(
// kForegroundOpacity,
// ),
// borderRadius: kBorderRadius8,
// ),
// TODO: Show error message in Global Status bar
// showErrorMessage: true,
isFormatting: true,
controller: controller,
focusNode: editorFocusNode,
keyboardType: TextInputType.multiline,
expands: true,
maxLines: null,
readOnly: widget.readOnly,
style: kCodeStyle.copyWith(
fontSize: Theme.of(context).textTheme.bodyMedium?.fontSize,
),
textAlignVertical: TextAlignVertical.top,
onChanged: widget.onChanged,
onTapOutside: (PointerDownEvent event) {
editorFocusNode.unfocus();
},
decoration: InputDecoration(
hintText: widget.hintText ?? kHintContent,
hintStyle: TextStyle(
color: Theme.of(context).colorScheme.outlineVariant,
),
borderRadius: kBorderRadius8,
),
showErrorMessage: true,
isFormatting: true,
key: Key(widget.fieldKey),
controller: controller,
focusNode: editorFocusNode,
keyboardType: TextInputType.multiline,
expands: true,
maxLines: null,
style: kCodeStyle,
textAlignVertical: TextAlignVertical.top,
onChanged: (value) {
controller.formatJson(sortJson: false);
widget.onChanged?.call(value);
},
decoration: InputDecoration(
hintText: kHintJson,
hintStyle: TextStyle(
color: Theme.of(context).colorScheme.outline.withOpacity(
kHintOpacity,
focusedBorder: OutlineInputBorder(
borderRadius: kBorderRadius8,
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outlineVariant,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: kBorderRadius8,
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary.withOpacity(
kHintOpacity,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: kBorderRadius8,
borderSide: BorderSide(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
),
),
filled: true,
hoverColor: kColorTransparent,
fillColor: Theme.of(context).colorScheme.surfaceContainerLowest,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: kBorderRadius8,
borderSide: BorderSide(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
),
),
filled: true,
hoverColor: kColorTransparent,
fillColor: Color.alphaBlend(
(Theme.of(context).brightness == Brightness.dark
? Theme.of(context).colorScheme.onPrimaryContainer
: Theme.of(context).colorScheme.primaryContainer)
.withOpacity(kForegroundOpacity),
Theme.of(context).colorScheme.surface),
),
),
Align(
alignment: Alignment.topRight,
child: ADIconButton(
icon: Icons.format_align_left,
tooltip: "Format JSON",
onPressed: () {
controller.formatJson(sortJson: false);
widget.onChanged?.call(controller.text);
},
),
),
],
);
}
}

View File

@@ -1,3 +1,6 @@
// Deprecated but kept as a backup
/*
import 'package:apidash_design_system/apidash_design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
@@ -79,20 +82,9 @@ class _HeaderFieldState extends State<HeaderField> {
style: kCodeStyle.copyWith(
color: colorScheme.onSurface,
),
decoration: InputDecoration(
hintStyle: kCodeStyle.copyWith(color: colorScheme.outlineVariant),
decoration: getTextFieldInputDecoration(
colorScheme,
hintText: widget.hintText,
contentPadding: const EdgeInsets.only(bottom: 12),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: colorScheme.outline,
),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: colorScheme.surfaceContainerHighest,
),
),
),
),
);
@@ -121,3 +113,4 @@ class _HeaderFieldState extends State<HeaderField> {
return getHeaderSuggestions(pattern);
}
}
*/

View File

@@ -0,0 +1,71 @@
import 'package:flutter/material.dart';
import 'package:apidash_design_system/apidash_design_system.dart';
class HeaderSuggestions extends StatefulWidget {
const HeaderSuggestions({
super.key,
required this.suggestionsCallback,
required this.query,
required this.onSuggestionTap,
});
final Future<List<String>?> Function(String) suggestionsCallback;
final String query;
final ValueSetter<String> onSuggestionTap;
@override
State<HeaderSuggestions> createState() => _HeaderSuggestionsState();
}
class _HeaderSuggestionsState extends State<HeaderSuggestions> {
List<String>? suggestions;
@override
void initState() {
super.initState();
widget.suggestionsCallback(widget.query).then((value) {
if (mounted) {
setState(() {
suggestions = value;
});
}
});
}
@override
void didUpdateWidget(HeaderSuggestions oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.query != widget.query) {
widget.suggestionsCallback(widget.query).then((value) {
if (mounted) {
setState(() {
suggestions = value;
});
}
});
}
}
@override
Widget build(BuildContext context) {
if (suggestions == null) {
return const SizedBox.shrink();
}
return suggestions!.isEmpty
? const SizedBox.shrink()
: SuggestionsMenuBox(
child: ListView.separated(
shrinkWrap: true,
itemCount: suggestions!.length,
separatorBuilder: (context, index) => const Divider(height: 2),
itemBuilder: (context, index) {
final suggestion = suggestions![index];
return ListTile(
dense: true,
title: Text(suggestion),
onTap: () => widget.onSuggestionTap(suggestion),
);
},
),
);
}
}

View File

@@ -63,13 +63,16 @@ class _RequestPaneState extends State<RequestPane>
widget.codePaneVisible
? Icons.code_off_rounded
: Icons.code_rounded,
size: 18,
),
label: SizedBox(
width: 75,
width: 80,
child: Text(
widget.codePaneVisible
? kLabelHideCode
: kLabelViewCode,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
),

View File

@@ -30,13 +30,13 @@ export 'editor.dart';
export 'error_message.dart';
export 'field_cell_obscurable.dart';
export 'field_cell.dart';
export 'field_header.dart';
export 'field_json_search.dart';
export 'field_read_only.dart';
export 'field_url.dart';
export 'intro_message.dart';
export 'json_previewer.dart';
export 'markdown.dart';
export 'menu_header_suggestions.dart';
export 'menu_item_card.dart';
export 'menu_sidebar_top.dart';
export 'overlay_widget.dart';

View File

@@ -1,5 +1,4 @@
import 'dart:io';
import 'dart:collection';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';
@@ -15,7 +14,7 @@ class HttpClientManager {
static final HttpClientManager _instance = HttpClientManager._internal();
static const int _maxCancelledRequests = 100;
final Map<String, http.Client> _clients = {};
final Queue<String> _cancelledRequests = Queue();
final Set<String> _cancelledRequests = {};
factory HttpClientManager() {
return _instance;
@@ -38,9 +37,9 @@ class HttpClientManager {
_clients[requestId]?.close();
_clients.remove(requestId);
_cancelledRequests.addLast(requestId);
while (_cancelledRequests.length > _maxCancelledRequests) {
_cancelledRequests.removeFirst();
_cancelledRequests.add(requestId);
if (_cancelledRequests.length > _maxCancelledRequests) {
_cancelledRequests.remove(_cancelledRequests.first);
}
}
}
@@ -49,6 +48,10 @@ class HttpClientManager {
return _cancelledRequests.contains(requestId);
}
void removeCancelledRequest(String requestId) {
_cancelledRequests.remove(requestId);
}
void closeClient(String requestId) {
if (_clients.containsKey(requestId)) {
_clients[requestId]?.close();

View File

@@ -19,6 +19,9 @@ Future<(HttpResponse?, Duration?, String?)> sendHttpRequest(
SupportedUriSchemes defaultUriScheme = kDefaultUriScheme,
bool noSSL = false,
}) async {
if (httpClientManager.wasRequestCancelled(requestId)) {
httpClientManager.removeCancelledRequest(requestId);
}
final client = httpClientManager.createClient(requestId, noSSL: noSSL);
(Uri?, String?) uriRec = getValidRequestUri(
@@ -71,37 +74,27 @@ Future<(HttpResponse?, Duration?, String?)> sendHttpRequest(
}
}
http.StreamedResponse multiPartResponse =
await multiPartRequest.send();
await client.send(multiPartRequest);
stopwatch.stop();
http.Response convertedMultiPartResponse =
await convertStreamedResponse(multiPartResponse);
return (convertedMultiPartResponse, stopwatch.elapsed, null);
}
}
switch (requestModel.method) {
case HTTPVerb.get:
response = await client.get(requestUrl, headers: headers);
break;
case HTTPVerb.head:
response = await client.head(requestUrl, headers: headers);
break;
case HTTPVerb.post:
response =
await client.post(requestUrl, headers: headers, body: body);
break;
case HTTPVerb.put:
response =
await client.put(requestUrl, headers: headers, body: body);
break;
case HTTPVerb.patch:
response =
await client.patch(requestUrl, headers: headers, body: body);
break;
case HTTPVerb.delete:
response =
await client.delete(requestUrl, headers: headers, body: body);
break;
}
response = switch (requestModel.method) {
HTTPVerb.get => await client.get(requestUrl, headers: headers),
HTTPVerb.head => response =
await client.head(requestUrl, headers: headers),
HTTPVerb.post => response =
await client.post(requestUrl, headers: headers, body: body),
HTTPVerb.put => response =
await client.put(requestUrl, headers: headers, body: body),
HTTPVerb.patch => response =
await client.patch(requestUrl, headers: headers, body: body),
HTTPVerb.delete => response =
await client.delete(requestUrl, headers: headers, body: body),
};
}
if (apiType == APIType.graphql) {
var requestBody = getGraphQLBody(requestModel);

View File

@@ -34,7 +34,7 @@ class ADCheckBox extends StatelessWidget {
if (states.contains(WidgetState.selected)) {
return colorScheme.primary;
}
return null;
return colorScheme.surfaceContainerLowest;
},
));
}

View File

@@ -10,6 +10,7 @@ class ADDropdownButton<T> extends StatelessWidget {
this.isExpanded = false,
this.isDense = false,
this.iconSize,
this.fontSize,
this.dropdownMenuItemPadding = kPs8,
this.dropdownMenuItemtextStyle,
});
@@ -20,6 +21,7 @@ class ADDropdownButton<T> extends StatelessWidget {
final bool isExpanded;
final bool isDense;
final double? iconSize;
final double? fontSize;
final EdgeInsetsGeometry dropdownMenuItemPadding;
final TextStyle? Function(T)? dropdownMenuItemtextStyle;
@@ -38,6 +40,7 @@ class ADDropdownButton<T> extends StatelessWidget {
elevation: 4,
style: kCodeStyle.copyWith(
color: Theme.of(context).colorScheme.primary,
fontSize: fontSize ?? Theme.of(context).textTheme.bodyMedium?.fontSize,
),
underline: Container(
height: 0,

View File

@@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import '../tokens/colors.dart';
enum ListTileType { switchOnOff, checkbox, button }
class ADListTile extends StatelessWidget {
const ADListTile({
super.key,
required this.type,
this.hoverColor = kColorTransparent,
required this.title,
this.subtitle,
this.value,
this.onChanged,
});
final ListTileType type;
final Color hoverColor;
final String title;
final String? subtitle;
// For Switch and checkbox tiles
final bool? value;
// For Switch and checkbox tiles
final Function(bool?)? onChanged;
@override
Widget build(BuildContext context) {
return switch (type) {
ListTileType.switchOnOff => SwitchListTile(
hoverColor: hoverColor,
title: Text(title),
subtitle: subtitle == null ? null : Text(subtitle ?? ''),
value: value ?? false,
onChanged: onChanged,
),
// TODO: Handle this case.
ListTileType.checkbox => throw UnimplementedError(),
// TODO: Handle this case.
ListTileType.button => throw UnimplementedError(),
};
}
}

View File

@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import '../tokens/tokens.dart';
class SuggestionsMenuBox extends StatelessWidget {
const SuggestionsMenuBox({
super.key,
required this.child,
this.width,
this.maxHeight,
});
final Widget child;
final double? width;
final double? maxHeight;
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: kBorderRadius8,
child: Material(
type: MaterialType.card,
elevation: 8,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: maxHeight ?? 200.0),
child: Ink(
width: width ?? 300.0,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: kBorderRadius8,
border: Border.all(
color: Theme.of(context).colorScheme.outlineVariant,
),
),
child: child,
),
),
),
);
}
}

View File

@@ -4,7 +4,9 @@ export 'button_text.dart';
export 'checkbox.dart';
export 'decoration_input_textfield.dart';
export 'dropdown.dart';
export 'list_tile.dart';
export 'popup_menu.dart';
export 'snackbar.dart';
export 'suggestions_menu_box.dart';
export 'textfield_outlined.dart';
export 'textfield_raw.dart';

View File

@@ -536,54 +536,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.21.2"
flutter_keyboard_visibility:
dependency: transitive
description:
name: flutter_keyboard_visibility
sha256: "98664be7be0e3ffca00de50f7f6a287ab62c763fc8c762e0a21584584a3ff4f8"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_keyboard_visibility_linux:
dependency: transitive
description:
name: flutter_keyboard_visibility_linux
sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
flutter_keyboard_visibility_macos:
dependency: transitive
description:
name: flutter_keyboard_visibility_macos
sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086
url: "https://pub.dev"
source: hosted
version: "1.0.0"
flutter_keyboard_visibility_platform_interface:
dependency: transitive
description:
name: flutter_keyboard_visibility_platform_interface
sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4
url: "https://pub.dev"
source: hosted
version: "2.0.0"
flutter_keyboard_visibility_web:
dependency: transitive
description:
name: flutter_keyboard_visibility_web
sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1
url: "https://pub.dev"
source: hosted
version: "2.0.0"
flutter_keyboard_visibility_windows:
dependency: transitive
description:
name: flutter_keyboard_visibility_windows
sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73
url: "https://pub.dev"
source: hosted
version: "1.0.0"
flutter_launcher_icons:
dependency: "direct dev"
description:
@@ -645,14 +597,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_typeahead:
dependency: "direct main"
description:
name: flutter_typeahead
sha256: d64712c65db240b1057559b952398ebb6e498077baeebf9b0731dade62438a6d
url: "https://pub.dev"
source: hosted
version: "5.2.0"
flutter_web_plugins:
dependency: transitive
description: flutter
@@ -1232,38 +1176,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pointer_interceptor:
dependency: transitive
description:
name: pointer_interceptor
sha256: "57210410680379aea8b1b7ed6ae0c3ad349bfd56fe845b8ea934a53344b9d523"
url: "https://pub.dev"
source: hosted
version: "0.10.1+2"
pointer_interceptor_ios:
dependency: transitive
description:
name: pointer_interceptor_ios
sha256: a6906772b3205b42c44614fcea28f818b1e5fdad73a4ca742a7bd49818d9c917
url: "https://pub.dev"
source: hosted
version: "0.10.1"
pointer_interceptor_platform_interface:
dependency: transitive
description:
name: pointer_interceptor_platform_interface
sha256: "0597b0560e14354baeb23f8375cd612e8bd4841bf8306ecb71fcd0bb78552506"
url: "https://pub.dev"
source: hosted
version: "0.10.0+1"
pointer_interceptor_web:
dependency: transitive
description:
name: pointer_interceptor_web
sha256: "7a7087782110f8c1827170660b09f8aa893e0e9a61431dbbe2ac3fc482e8c044"
url: "https://pub.dev"
source: hosted
version: "0.10.2+1"
pool:
dependency: transitive
description:

View File

@@ -28,7 +28,6 @@ dependencies:
flutter_portal: ^1.1.4
flutter_riverpod: ^2.5.1
flutter_svg: ^2.0.17
flutter_typeahead: ^5.2.0
fvp: ^0.30.0
highlighter: ^0.1.1
hive_flutter: ^1.1.0

View File

@@ -0,0 +1,50 @@
import 'package:apidash/screens/common_widgets/envfield_header.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:extended_text_field/extended_text_field.dart';
import 'package:spot/spot.dart';
void main() {
group('HeaderField Widget Tests', () {
testWidgets('HeaderField renders and displays ExtendedTextField',
(tester) async {
await tester.pumpWidget(
const Portal(
child: MaterialApp(
home: Scaffold(
body: EnvHeaderField(
keyId: "testKey",
hintText: "Enter header",
),
),
),
),
);
spot<EnvHeaderField>().spot<ExtendedTextField>().existsOnce();
});
testWidgets('HeaderField calls onChanged when text changes',
(tester) async {
String? changedText;
await tester.pumpWidget(
Portal(
child: MaterialApp(
home: Scaffold(
body: EnvHeaderField(
keyId: "testKey",
hintText: "Enter header",
onChanged: (text) => changedText = text,
),
),
),
),
);
await act.tap(spot<EnvHeaderField>().spot<ExtendedTextField>());
tester.testTextInput.enterText("new header");
expect(changedText, "new header");
});
});
}

View File

@@ -3,6 +3,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:apidash/widgets/field_header.dart';
void main() {
/* This HeaderField is deprecated
testWidgets('Testing Header Field', (tester) async {
await tester.pumpWidget(
const MaterialApp(
@@ -21,4 +22,5 @@ void main() {
expect(find.byKey(const Key("1")), findsOneWidget);
expect(find.text('X'), findsOneWidget);
});
*/
}

View File

@@ -0,0 +1,61 @@
import 'package:apidash/widgets/menu_header_suggestions.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('HeaderSuggestions Widget Tests', () {
testWidgets('HeaderSuggestions displays suggestions correctly',
(tester) async {
await tester.pumpWidget(
MaterialApp(
home: HeaderSuggestions(
query: "header",
suggestionsCallback: (query) async => ["header1", "header2"],
onSuggestionTap: (suggestion) {
expect(suggestion, "header1");
},
),
),
);
await tester.pumpAndSettle();
expect(find.byType(ListTile), findsNWidgets(2));
expect(find.text("header1"), findsOneWidget);
expect(find.text("header2"), findsOneWidget);
});
testWidgets('HeaderSuggestions calls onSuggestionTap when tapped',
(tester) async {
String? selectedSuggestion;
await tester.pumpWidget(
MaterialApp(
home: HeaderSuggestions(
query: "header",
suggestionsCallback: (query) async => ["header1"],
onSuggestionTap: (suggestion) => selectedSuggestion = suggestion,
),
),
);
await tester.pumpAndSettle();
await tester.tap(find.text("header1"));
expect(selectedSuggestion, "header1");
});
testWidgets('HeaderSuggestions shows no suggestions when list is empty',
(tester) async {
await tester.pumpWidget(
MaterialApp(
home: HeaderSuggestions(
query: "test",
suggestionsCallback: (query) async => [],
onSuggestionTap: (suggestion) {},
),
),
);
await tester.pumpAndSettle();
expect(find.byType(ListTile), findsNothing);
});
});
}