diff --git a/integration_test/req_helper.dart b/integration_test/req_helper.dart index 4a18745a..b16a89a8 100644 --- a/integration_test/req_helper.dart +++ b/integration_test/req_helper.dart @@ -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)); diff --git a/lib/screens/common_widgets/common_widgets.dart b/lib/screens/common_widgets/common_widgets.dart index d6932407..fc4c66b8 100644 --- a/lib/screens/common_widgets/common_widgets.dart +++ b/lib/screens/common_widgets/common_widgets.dart @@ -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'; diff --git a/lib/screens/common_widgets/env_trigger_field.dart b/lib/screens/common_widgets/env_trigger_field.dart index 2fe71840..a7e470e7 100644 --- a/lib/screens/common_widgets/env_trigger_field.dart +++ b/lib/screens/common_widgets/env_trigger_field.dart @@ -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 createState() => @@ -30,21 +39,24 @@ class EnvironmentTriggerField extends StatefulWidget { } class EnvironmentTriggerFieldState extends State { - 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 { 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 { 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 { onSubmitted: widget.onFieldSubmitted, specialTextSpanBuilder: EnvRegExpSpanBuilder(), onTapOutside: (event) { - focusNode.unfocus(); + _focusNode.unfocus(); }, ); }, diff --git a/lib/screens/common_widgets/env_trigger_options.dart b/lib/screens/common_widgets/env_trigger_options.dart index 045db973..d6dcf553 100644 --- a/lib/screens/common_widgets/env_trigger_options.dart +++ b/lib/screens/common_widgets/env_trigger_options.dart @@ -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), + ); + }, ), ); } diff --git a/lib/screens/common_widgets/envfield_cell.dart b/lib/screens/common_widgets/envfield_cell.dart index b99f4a7e..ececb8a2 100644 --- a/lib/screens/common_widgets/envfield_cell.dart +++ b/lib/screens/common_widgets/envfield_cell.dart @@ -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, ); } diff --git a/lib/screens/common_widgets/envfield_header.dart b/lib/screens/common_widgets/envfield_header.dart new file mode 100644 index 00000000..5975f1ad --- /dev/null +++ b/lib/screens/common_widgets/envfield_header.dart @@ -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 createState() => _EnvHeaderFieldState(); +} + +class _EnvHeaderFieldState extends State { + 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?> headerSuggestionCallback(String pattern) async { + if (pattern.isEmpty) { + return null; + } + return getHeaderSuggestions(pattern) + .where( + (suggestion) => suggestion.toLowerCase() != pattern.toLowerCase()) + .toList(); + } +} diff --git a/lib/screens/common_widgets/envfield_url.dart b/lib/screens/common_widgets/envfield_url.dart index 6e6b3706..4757c870 100644 --- a/lib/screens/common_widgets/envfield_url.dart +++ b/lib/screens/common_widgets/envfield_url.dart @@ -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, diff --git a/lib/screens/history/history_widgets/his_request_pane.dart b/lib/screens/history/history_widgets/his_request_pane.dart index dc618e77..a64b96f8 100644 --- a/lib/screens/history/history_widgets/his_request_pane.dart +++ b/lib/screens/history/history_widgets/his_request_pane.dart @@ -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", diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart index c679f277..8c5a805d 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart @@ -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 diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_headers.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_headers.dart index ecb3f6da..658789a0 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_headers.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_headers.dart @@ -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 { ), ), DataCell( - HeaderField( + EnvHeaderField( keyId: "$selectedId-$index-headers-k-$seed", initialValue: headerRows[index].name, hintText: kHintAddName, diff --git a/lib/screens/settings_page.dart b/lib/screens/settings_page.dart index eb4b6008..ca72f886 100644 --- a/lib/screens/settings_page.dart +++ b/lib/screens/settings_page.dart @@ -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 diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index a9f96732..87720f61 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -102,7 +102,7 @@ class _TextFieldEditorState extends State { ), filled: true, hoverColor: kColorTransparent, - fillColor: Theme.of(context).colorScheme.surfaceContainerLow, + fillColor: Theme.of(context).colorScheme.surfaceContainerLowest, ), ), ); diff --git a/lib/widgets/editor_json.dart b/lib/widgets/editor_json.dart index 3c0c2abf..3e4c2425 100644 --- a/lib/widgets/editor_json.dart +++ b/lib/widgets/editor_json.dart @@ -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 createState() => _JsonTextFieldEditorState(); } @@ -44,6 +48,9 @@ class _JsonTextFieldEditorState extends State { @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 { } @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: { - 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: { + 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); + }, + ), + ), + ], ); } } diff --git a/lib/widgets/field_header.dart b/lib/widgets/field_header.dart index 896fa416..12e1e2aa 100644 --- a/lib/widgets/field_header.dart +++ b/lib/widgets/field_header.dart @@ -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 { 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 { return getHeaderSuggestions(pattern); } } +*/ diff --git a/lib/widgets/menu_header_suggestions.dart b/lib/widgets/menu_header_suggestions.dart new file mode 100644 index 00000000..6aec9aa5 --- /dev/null +++ b/lib/widgets/menu_header_suggestions.dart @@ -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?> Function(String) suggestionsCallback; + final String query; + final ValueSetter onSuggestionTap; + + @override + State createState() => _HeaderSuggestionsState(); +} + +class _HeaderSuggestionsState extends State { + List? 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), + ); + }, + ), + ); + } +} diff --git a/lib/widgets/request_pane.dart b/lib/widgets/request_pane.dart index 95871d53..09ebd0a3 100644 --- a/lib/widgets/request_pane.dart +++ b/lib/widgets/request_pane.dart @@ -63,13 +63,16 @@ class _RequestPaneState extends State 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, ), ), ), diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index f6427604..634ab77e 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -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'; diff --git a/packages/apidash_core/lib/services/http_client_manager.dart b/packages/apidash_core/lib/services/http_client_manager.dart index bec23214..7b815413 100644 --- a/packages/apidash_core/lib/services/http_client_manager.dart +++ b/packages/apidash_core/lib/services/http_client_manager.dart @@ -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 _clients = {}; - final Queue _cancelledRequests = Queue(); + final Set _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(); diff --git a/packages/apidash_core/lib/services/http_service.dart b/packages/apidash_core/lib/services/http_service.dart index ad06a21d..73f82d9a 100644 --- a/packages/apidash_core/lib/services/http_service.dart +++ b/packages/apidash_core/lib/services/http_service.dart @@ -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); diff --git a/packages/apidash_design_system/lib/widgets/checkbox.dart b/packages/apidash_design_system/lib/widgets/checkbox.dart index 1707b051..eca9f257 100644 --- a/packages/apidash_design_system/lib/widgets/checkbox.dart +++ b/packages/apidash_design_system/lib/widgets/checkbox.dart @@ -34,7 +34,7 @@ class ADCheckBox extends StatelessWidget { if (states.contains(WidgetState.selected)) { return colorScheme.primary; } - return null; + return colorScheme.surfaceContainerLowest; }, )); } diff --git a/packages/apidash_design_system/lib/widgets/dropdown.dart b/packages/apidash_design_system/lib/widgets/dropdown.dart index 8aa90590..5cd88c8a 100644 --- a/packages/apidash_design_system/lib/widgets/dropdown.dart +++ b/packages/apidash_design_system/lib/widgets/dropdown.dart @@ -10,6 +10,7 @@ class ADDropdownButton extends StatelessWidget { this.isExpanded = false, this.isDense = false, this.iconSize, + this.fontSize, this.dropdownMenuItemPadding = kPs8, this.dropdownMenuItemtextStyle, }); @@ -20,6 +21,7 @@ class ADDropdownButton 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 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, diff --git a/packages/apidash_design_system/lib/widgets/list_tile.dart b/packages/apidash_design_system/lib/widgets/list_tile.dart new file mode 100644 index 00000000..2948958f --- /dev/null +++ b/packages/apidash_design_system/lib/widgets/list_tile.dart @@ -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(), + }; + } +} diff --git a/packages/apidash_design_system/lib/widgets/suggestions_menu_box.dart b/packages/apidash_design_system/lib/widgets/suggestions_menu_box.dart new file mode 100644 index 00000000..5d0be2c2 --- /dev/null +++ b/packages/apidash_design_system/lib/widgets/suggestions_menu_box.dart @@ -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, + ), + ), + ), + ); + } +} diff --git a/packages/apidash_design_system/lib/widgets/widgets.dart b/packages/apidash_design_system/lib/widgets/widgets.dart index 1ebd0836..89131611 100644 --- a/packages/apidash_design_system/lib/widgets/widgets.dart +++ b/packages/apidash_design_system/lib/widgets/widgets.dart @@ -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'; diff --git a/pubspec.lock b/pubspec.lock index c072a0c7..9e5528c4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index af67db98..bd0b276e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 diff --git a/test/screens/common_widgets/envfield_header_test.dart b/test/screens/common_widgets/envfield_header_test.dart new file mode 100644 index 00000000..776648dd --- /dev/null +++ b/test/screens/common_widgets/envfield_header_test.dart @@ -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().spot().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().spot()); + tester.testTextInput.enterText("new header"); + expect(changedText, "new header"); + }); + }); +} diff --git a/test/widgets/headerfield_test.dart b/test/widgets/headerfield_test.dart index 4475058b..8071ea4f 100644 --- a/test/widgets/headerfield_test.dart +++ b/test/widgets/headerfield_test.dart @@ -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); }); + */ } diff --git a/test/widgets/menu_header_suggestions_test.dart b/test/widgets/menu_header_suggestions_test.dart new file mode 100644 index 00000000..1c8ed3bf --- /dev/null +++ b/test/widgets/menu_header_suggestions_test.dart @@ -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); + }); + }); +}