diff --git a/lib/screens/common_widgets/env_trigger_json_editing.dart b/lib/screens/common_widgets/env_trigger_json_editing.dart new file mode 100644 index 000000000..7af482f2d --- /dev/null +++ b/lib/screens/common_widgets/env_trigger_json_editing.dart @@ -0,0 +1,145 @@ +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 EnvironmentTriggerJsonEditor extends StatefulWidget { + const EnvironmentTriggerJsonEditor({ + 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() => + EnvironmentTriggerJsonEditorState(); +} + +class EnvironmentTriggerJsonEditorState + extends State { + late JsonTextFieldController controller; + late FocusNode _focusNode; + + @override + void initState() { + super.initState(); + if (widget.controller != null) { + controller = widget.controller as JsonTextFieldController; + } else { + controller = JsonTextFieldController(); + } + + _focusNode = widget.focusNode ?? + FocusNode(debugLabel: "env Trigger Editor Focus Node"); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + void didUpdateWidget(EnvironmentTriggerJsonEditor oldWidget) { + super.didUpdateWidget(oldWidget); + if ((oldWidget.keyId != widget.keyId) || + (oldWidget.initialValue != widget.initialValue)) { + if (widget.controller != null) { + controller = widget.controller as JsonTextFieldController; + } else { + controller = JsonTextFieldController(); + } + controller.text = widget.initialValue ?? ''; + } + } + + @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..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,3 +1,4 @@ +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'; @@ -5,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 { @@ -47,17 +49,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: EnvironmentTriggerJsonEditor( + 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/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 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)); + }); +}