From d17854debf9d02db056f2d7e85735a415135661d Mon Sep 17 00:00:00 2001 From: Mohamed Adel Kamal Date: Mon, 27 Oct 2025 17:03:01 +0300 Subject: [PATCH 1/2] Added Suggestions to json body requests --- .../env_trigger_text_editing.dart | 136 ++++++++++++++++++ .../request_pane/request_body.dart | 8 +- lib/utils/utils.dart | 2 +- lib/widgets/editor_json.dart | 10 +- 4 files changed, 148 insertions(+), 8 deletions(-) create mode 100644 lib/screens/common_widgets/env_trigger_text_editing.dart diff --git a/lib/screens/common_widgets/env_trigger_text_editing.dart b/lib/screens/common_widgets/env_trigger_text_editing.dart new file mode 100644 index 000000000..096b9e74c --- /dev/null +++ b/lib/screens/common_widgets/env_trigger_text_editing.dart @@ -0,0 +1,136 @@ +import 'package:apidash/widgets/editor_json.dart'; +import 'package:flutter/material.dart'; +import 'package:json_field_editor/json_field_editor.dart'; +import 'package:multi_trigger_autocomplete_plus/multi_trigger_autocomplete_plus.dart'; +import 'env_trigger_options.dart'; + +class EnvironmentTriggerTextEditor extends StatefulWidget { + const EnvironmentTriggerTextEditor({ + super.key, + required this.keyId, + this.initialValue, + this.controller, + this.focusNode, + this.onChanged, + this.onTextSubmitted, + this.style, + this.decoration, + this.optionsWidthFactor, + this.autocompleteNoTrigger, + this.readOnly = false, + this.obscureText = false, + }) : 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)? onTextSubmitted; + final TextStyle? style; + final InputDecoration? decoration; + final double? optionsWidthFactor; + final AutocompleteNoTrigger? autocompleteNoTrigger; + final bool readOnly; + final bool obscureText; + + @override + State createState() => + EnvironmentTriggerTextEditorState(); +} + +class EnvironmentTriggerTextEditorState + extends State { + late JsonTextFieldController controller; + late FocusNode _focusNode; + + @override + void initState() { + super.initState(); + controller = JsonTextFieldController(); + _focusNode = widget.focusNode ?? + FocusNode(debugLabel: "env Trigger Editor Focus Node"); + } + + @override + void dispose() { + controller.dispose(); + // _focusNode.dispose(); // the Json TextFieldEditor will dispose this if created here + super.dispose(); + } + + @override + void didUpdateWidget(EnvironmentTriggerTextEditor oldWidget) { + super.didUpdateWidget(oldWidget); + if ((oldWidget.keyId != widget.keyId) || + (oldWidget.initialValue != widget.initialValue)) { + controller = JsonTextFieldController(); + } + } + + @override + Widget build(BuildContext context) { + return MultiTriggerAutocomplete( + key: Key(widget.keyId), + textEditingController: controller, + focusNode: _focusNode, + optionsWidthFactor: widget.optionsWidthFactor ?? 1, + optionsAlignment: OptionsAlignment.topStart, + autocompleteTriggers: [ + if (widget.autocompleteNoTrigger != null) widget.autocompleteNoTrigger!, + AutocompleteTrigger( + trigger: '${controller.text}{', + triggerEnd: "}}", + triggerOnlyAfterSpace: false, + optionsViewBuilder: (context, autocompleteQuery, controller) { + return EnvironmentTriggerOptions( + query: autocompleteQuery.query, + onSuggestionTap: (suggestion) { + final autocomplete = MultiTriggerAutocomplete.of(context); + autocomplete.acceptAutocompleteOption( + '{${suggestion.variable.key}', + ); + widget.onChanged?.call(controller.text); + }, + ); + }, + ), + AutocompleteTrigger( + trigger: '${controller.text}{{', + triggerEnd: "}}", + triggerOnlyAfterSpace: true, + optionsViewBuilder: (context, autocompleteQuery, controller) { + return EnvironmentTriggerOptions( + query: autocompleteQuery.query, + onSuggestionTap: (suggestion) { + final autocomplete = MultiTriggerAutocomplete.of(context); + autocomplete.acceptAutocompleteOption( + '{${suggestion.variable.key}', + ); + widget.onChanged?.call(controller.text); + }, + ); + }, + ), + ], + fieldViewBuilder: (context, textEditingController, focusNode) { + return JsonTextFieldEditor( + key: Key("${widget.keyId}-json-body"), + fieldKey: "${widget.keyId}-json-body-editor", + isDark: Theme.of(context).brightness == Brightness.dark, + initialValue: widget.initialValue, + onChanged: (String value) { + widget.onChanged?.call(value); + }, + readOnly: widget.readOnly, + jsonTextFieldController: + textEditingController as JsonTextFieldController, + focusNode: focusNode, + ); + }, + ); + } +} 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 e192f26ee..764c1cbf5 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 @@ -1,3 +1,4 @@ +import 'package:apidash/screens/common_widgets/env_trigger_text_editing.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; @@ -47,17 +48,14 @@ class EditRequestBody extends ConsumerWidget { const Padding(padding: kPh4, child: FormDataWidget()), ContentType.json => Padding( padding: kPt5o10, - child: JsonTextFieldEditor( - key: Key("$selectedId-json-body"), - fieldKey: "$selectedId-json-body-editor-$darkMode", - isDark: darkMode, + child: EnvironmentTriggerTextEditor( + keyId: "$selectedId-environment-trigger", initialValue: requestModel?.httpRequestModel?.body, onChanged: (String value) { ref .read(collectionStateNotifierProvider.notifier) .update(body: value); }, - hintText: kHintJson, ), ), _ => Padding( diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 72b574689..00262ddfc 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -8,4 +8,4 @@ export 'http_utils.dart'; export 'js_utils.dart'; export 'save_utils.dart'; export 'ui_utils.dart'; -export 'window_utils.dart'; +export 'window_utils.dart'; \ No newline at end of file diff --git a/lib/widgets/editor_json.dart b/lib/widgets/editor_json.dart index 19f004ae6..c1bea7a6d 100644 --- a/lib/widgets/editor_json.dart +++ b/lib/widgets/editor_json.dart @@ -14,12 +14,16 @@ class JsonTextFieldEditor extends StatefulWidget { this.hintText, this.readOnly = false, this.isDark = false, + this.jsonTextFieldController, + this.focusNode, }); final String fieldKey; + final FocusNode? focusNode; final Function(String)? onChanged; final String? initialValue; final String? hintText; + final JsonTextFieldController ? jsonTextFieldController; final bool readOnly; final bool isDark; @@ -28,7 +32,7 @@ class JsonTextFieldEditor extends StatefulWidget { } class _JsonTextFieldEditorState extends State { - final JsonTextFieldController controller = JsonTextFieldController(); + late JsonTextFieldController controller; late final FocusNode editorFocusNode; void insertTab() { @@ -51,6 +55,8 @@ class _JsonTextFieldEditorState extends State { @override void initState() { super.initState(); + controller = + widget.jsonTextFieldController ?? JsonTextFieldController(); if (widget.initialValue != null) { controller.text = widget.initialValue!; } @@ -60,7 +66,7 @@ class _JsonTextFieldEditorState extends State { // controller.formatJson(sortJson: false); // setState(() {}); // }); - editorFocusNode = FocusNode(debugLabel: "Editor Focus Node"); + editorFocusNode = widget.focusNode ?? FocusNode(debugLabel: "Editor Focus Node"); } @override From ea2d61537e8bcc527ba52c12d0c22f32ea1b08a5 Mon Sep 17 00:00:00 2001 From: Mohamed Adel Kamal Date: Wed, 29 Oct 2025 12:23:27 +0300 Subject: [PATCH 2/2] Added Tests, Renamed env_trigger_text -> env_trigger_json --- ...ing.dart => env_trigger_json_editing.dart} | 29 +++-- .../request_pane/request_body.dart | 5 +- .../env_trigger_json_editing_test.dart | 117 ++++++++++++++++++ 3 files changed, 139 insertions(+), 12 deletions(-) rename lib/screens/common_widgets/{env_trigger_text_editing.dart => env_trigger_json_editing.dart} (85%) create mode 100644 test/screens/common_widgets/env_trigger_json_editing_test.dart diff --git a/lib/screens/common_widgets/env_trigger_text_editing.dart b/lib/screens/common_widgets/env_trigger_json_editing.dart similarity index 85% rename from lib/screens/common_widgets/env_trigger_text_editing.dart rename to lib/screens/common_widgets/env_trigger_json_editing.dart index 096b9e74c..7af482f2d 100644 --- a/lib/screens/common_widgets/env_trigger_text_editing.dart +++ b/lib/screens/common_widgets/env_trigger_json_editing.dart @@ -4,8 +4,8 @@ import 'package:json_field_editor/json_field_editor.dart'; import 'package:multi_trigger_autocomplete_plus/multi_trigger_autocomplete_plus.dart'; import 'env_trigger_options.dart'; -class EnvironmentTriggerTextEditor extends StatefulWidget { - const EnvironmentTriggerTextEditor({ +class EnvironmentTriggerJsonEditor extends StatefulWidget { + const EnvironmentTriggerJsonEditor({ super.key, required this.keyId, this.initialValue, @@ -38,19 +38,24 @@ class EnvironmentTriggerTextEditor extends StatefulWidget { final bool obscureText; @override - State createState() => - EnvironmentTriggerTextEditorState(); + State createState() => + EnvironmentTriggerJsonEditorState(); } -class EnvironmentTriggerTextEditorState - extends State { +class EnvironmentTriggerJsonEditorState + extends State { late JsonTextFieldController controller; late FocusNode _focusNode; @override void initState() { super.initState(); - controller = JsonTextFieldController(); + if (widget.controller != null) { + controller = widget.controller as JsonTextFieldController; + } else { + controller = JsonTextFieldController(); + } + _focusNode = widget.focusNode ?? FocusNode(debugLabel: "env Trigger Editor Focus Node"); } @@ -58,16 +63,20 @@ class EnvironmentTriggerTextEditorState @override void dispose() { controller.dispose(); - // _focusNode.dispose(); // the Json TextFieldEditor will dispose this if created here super.dispose(); } @override - void didUpdateWidget(EnvironmentTriggerTextEditor oldWidget) { + void didUpdateWidget(EnvironmentTriggerJsonEditor oldWidget) { super.didUpdateWidget(oldWidget); if ((oldWidget.keyId != widget.keyId) || (oldWidget.initialValue != widget.initialValue)) { - controller = JsonTextFieldController(); + if (widget.controller != null) { + controller = widget.controller as JsonTextFieldController; + } else { + controller = JsonTextFieldController(); + } + controller.text = widget.initialValue ?? ''; } } 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 764c1cbf5..bddaef69f 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 @@ -1,4 +1,4 @@ -import 'package:apidash/screens/common_widgets/env_trigger_text_editing.dart'; +import 'package:apidash/screens/common_widgets/env_trigger_json_editing.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; @@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; +import 'package:json_field_editor/json_field_editor.dart'; import 'request_form_data.dart'; class EditRequestBody extends ConsumerWidget { @@ -48,7 +49,7 @@ class EditRequestBody extends ConsumerWidget { const Padding(padding: kPh4, child: FormDataWidget()), ContentType.json => Padding( padding: kPt5o10, - child: EnvironmentTriggerTextEditor( + child: EnvironmentTriggerJsonEditor( keyId: "$selectedId-environment-trigger", initialValue: requestModel?.httpRequestModel?.body, onChanged: (String value) { diff --git a/test/screens/common_widgets/env_trigger_json_editing_test.dart b/test/screens/common_widgets/env_trigger_json_editing_test.dart new file mode 100644 index 000000000..f47754518 --- /dev/null +++ b/test/screens/common_widgets/env_trigger_json_editing_test.dart @@ -0,0 +1,117 @@ +import 'package:apidash/widgets/editor_json.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_portal/flutter_portal.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/screens/common_widgets/env_trigger_json_editing.dart'; + +main() { + const envMap = { + kGlobalEnvironmentId: [ + EnvironmentVariableModel(key: 'key1', value: 'value1'), + EnvironmentVariableModel(key: 'key2', value: 'value2'), + ], + 'activeEnvId': [ + EnvironmentVariableModel(key: 'key2', value: 'value1'), + EnvironmentVariableModel(key: 'key3', value: 'value2'), + ], + }; + + const suggestions = [ + EnvironmentVariableSuggestion( + environmentId: 'activeEnvId', + variable: EnvironmentVariableModel(key: 'key2', value: 'value1'), + ), + EnvironmentVariableSuggestion( + environmentId: 'activeEnvId', + variable: EnvironmentVariableModel(key: 'key3', value: 'value2'), + ), + EnvironmentVariableSuggestion( + environmentId: kGlobalEnvironmentId, + variable: EnvironmentVariableModel(key: 'key1', value: 'value1'), + ), + EnvironmentVariableSuggestion( + environmentId: kGlobalEnvironmentId, + variable: EnvironmentVariableModel(key: 'key2', value: 'value2'), + ), + ]; + + testWidgets('EnvironmentTriggerJsonEditor updates controller text', + (WidgetTester tester) async { + final fieldKey = GlobalKey(); + const initialValue = 'initial'; + const updatedValue = 'updated'; + + await tester.pumpWidget( + Portal( + child: MaterialApp( + home: Scaffold( + body: EnvironmentTriggerJsonEditor( + key: fieldKey, + keyId: 'testKey', + initialValue: initialValue, + ), + ), + ), + ), + ); + + Finder field = find.byType(JsonTextFieldEditor); + expect(field, findsOneWidget); + expect(fieldKey.currentState!.controller.text, initialValue); + + await tester.pumpWidget( + Portal( + child: MaterialApp( + home: Scaffold( + body: EnvironmentTriggerJsonEditor( + key: fieldKey, + keyId: 'testKey', + initialValue: updatedValue, + ), + ), + ), + ), + ); + + expect(fieldKey.currentState!.controller.text, updatedValue); + }); + + testWidgets( + 'EnvironmentTriggerJsonEditor shows suggestions when trigger typed', + (WidgetTester tester) async { + final fieldKey = GlobalKey(); + const textWithSuggestionTrigger = '{"Test" : {{'; + + await tester.pumpWidget( + ProviderScope( + overrides: [ + availableEnvironmentVariablesStateProvider + .overrideWith((ref) => envMap), + activeEnvironmentIdStateProvider.overrideWith((ref) => 'activeEnvId'), + ], + child: Portal( + child: MaterialApp( + home: Scaffold( + body: EnvironmentTriggerJsonEditor( + key: fieldKey, + keyId: 'testKey', + initialValue: textWithSuggestionTrigger, + ), + ), + ), + ), + ), + ); + + await tester.tap(find.byType(JsonTextFieldEditor)); + await tester.pumpAndSettle(); + + expect(find.byType(ClipRRect), findsOneWidget); + expect(find.byType(ListView), findsOneWidget); + expect(find.byType(ListTile), findsNWidgets(3)); + }); +}