diff --git a/doc/dev_guide/api_endpoints_for_testing.md b/doc/dev_guide/api_endpoints_for_testing.md index 7f2e9556..5e0c1378 100644 --- a/doc/dev_guide/api_endpoints_for_testing.md +++ b/doc/dev_guide/api_endpoints_for_testing.md @@ -11,7 +11,10 @@ A List of API endpoints that can be used for testing API Dash #### For Testing HTTP PUT, PATCH, DELETE - https://reqres.in/ - + +#### For Testing HTTP OPTIONS +- https://reqbin.com/echo/options + #### For Testing sites with Bad Certificate - https://badssl.com/ - https://www.ssl.com/sample-valid-revoked-and-expired-ssl-tls-certificates/ diff --git a/lib/dashbot/features/explain.dart b/lib/dashbot/features/explain.dart index 4b008e6c..74e30248 100644 --- a/lib/dashbot/features/explain.dart +++ b/lib/dashbot/features/explain.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import '../services/dashbot_service.dart'; import 'package:apidash/models/request_model.dart'; @@ -19,20 +18,14 @@ class ExplainFeature { return "Error: Invalid API request (missing endpoint)."; } - final method = requestModel.httpRequestModel?.method - .toString() - .split('.') - .last - .toUpperCase() ?? - "GET"; - final endpoint = requestModel.httpRequestModel!.url; + final method = + requestModel.httpRequestModel?.method.name.toUpperCase() ?? "GET"; + final url = requestModel.httpRequestModel!.url; final headers = requestModel.httpRequestModel?.enabledHeadersMap ?? {}; final parameters = requestModel.httpRequestModel?.enabledParamsMap ?? {}; - final body = requestModel.httpRequestModel?.body; - final rawResponse = responseModel.body; - final responseBody = - rawResponse is String ? rawResponse : jsonEncode(rawResponse); - final statusCode = responseModel.statusCode ?? 0; + final body = requestModel.httpRequestModel?.body ?? ''; + final responseBody = responseModel.body; + final statusCode = responseModel.statusCode; final prompt = ''' FOCUSED API INTERACTION BREAKDOWN @@ -41,10 +34,16 @@ FOCUSED API INTERACTION BREAKDOWN - Endpoint Purpose: What is this API endpoint designed to do? - Interaction Type: Describe the core purpose of this specific request -**Request Mechanics:** -- Exact Endpoint: $endpoint +**Request Details:** +- Endpoint: $url - HTTP Method: $method -- Key Parameters: ${parameters.isNotEmpty ? 'Specific inputs driving the request' : 'No custom parameters'} +- Request Headers: ${headers.isEmpty ? "None" : headers} +- URL Parameters: ${parameters.isEmpty ? "None" : parameters} +- Request Body: ${body.isEmpty ? "None" : body} + +**Response Details** +- Status Code: $statusCode +- Content: $responseBody **Response CORE Insights:** - Status: Success or Failure? diff --git a/lib/models/history_meta_model.g.dart b/lib/models/history_meta_model.g.dart index a2b2b7b1..da184793 100644 --- a/lib/models/history_meta_model.g.dart +++ b/lib/models/history_meta_model.g.dart @@ -44,4 +44,5 @@ const _$HTTPVerbEnumMap = { HTTPVerb.put: 'put', HTTPVerb.patch: 'patch', HTTPVerb.delete: 'delete', + HTTPVerb.options: 'options', }; diff --git a/lib/models/settings_model.dart b/lib/models/settings_model.dart index 74bc2ff9..a06b1e59 100644 --- a/lib/models/settings_model.dart +++ b/lib/models/settings_model.dart @@ -17,6 +17,7 @@ class SettingsModel { this.historyRetentionPeriod = HistoryRetentionPeriod.oneWeek, this.workspaceFolderPath, this.isSSLDisabled = false, + this.isDashBotEnabled = true, }); final bool isDark; @@ -31,6 +32,7 @@ class SettingsModel { final HistoryRetentionPeriod historyRetentionPeriod; final String? workspaceFolderPath; final bool isSSLDisabled; + final bool isDashBotEnabled; SettingsModel copyWith({ bool? isDark, @@ -45,6 +47,7 @@ class SettingsModel { HistoryRetentionPeriod? historyRetentionPeriod, String? workspaceFolderPath, bool? isSSLDisabled, + bool? isDashBotEnabled, }) { return SettingsModel( isDark: isDark ?? this.isDark, @@ -61,6 +64,7 @@ class SettingsModel { historyRetentionPeriod ?? this.historyRetentionPeriod, workspaceFolderPath: workspaceFolderPath ?? this.workspaceFolderPath, isSSLDisabled: isSSLDisabled ?? this.isSSLDisabled, + isDashBotEnabled: isDashBotEnabled ?? this.isDashBotEnabled, ); } @@ -80,6 +84,7 @@ class SettingsModel { historyRetentionPeriod: historyRetentionPeriod, workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, + isDashBotEnabled: isDashBotEnabled, ); } @@ -134,6 +139,7 @@ class SettingsModel { } final workspaceFolderPath = data["workspaceFolderPath"] as String?; final isSSLDisabled = data["isSSLDisabled"] as bool?; + final isDashBotEnabled = data["isDashBotEnabled"] as bool?; const sm = SettingsModel(); @@ -151,6 +157,7 @@ class SettingsModel { historyRetentionPeriod ?? HistoryRetentionPeriod.oneWeek, workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, + isDashBotEnabled: isDashBotEnabled, ); } @@ -170,6 +177,7 @@ class SettingsModel { "historyRetentionPeriod": historyRetentionPeriod.name, "workspaceFolderPath": workspaceFolderPath, "isSSLDisabled": isSSLDisabled, + "isDashBotEnabled": isDashBotEnabled, }; } @@ -194,7 +202,8 @@ class SettingsModel { other.activeEnvironmentId == activeEnvironmentId && other.historyRetentionPeriod == historyRetentionPeriod && other.workspaceFolderPath == workspaceFolderPath && - other.isSSLDisabled == isSSLDisabled; + other.isSSLDisabled == isSSLDisabled && + other.isDashBotEnabled == isDashBotEnabled; } @override @@ -213,6 +222,7 @@ class SettingsModel { historyRetentionPeriod, workspaceFolderPath, isSSLDisabled, + isDashBotEnabled, ); } } diff --git a/lib/providers/settings_providers.dart b/lib/providers/settings_providers.dart index 6b64343a..d3cb9f2f 100644 --- a/lib/providers/settings_providers.dart +++ b/lib/providers/settings_providers.dart @@ -33,6 +33,7 @@ class ThemeStateNotifier extends StateNotifier { HistoryRetentionPeriod? historyRetentionPeriod, String? workspaceFolderPath, bool? isSSLDisabled, + bool? isDashBotEnabled, }) async { state = state.copyWith( isDark: isDark, @@ -47,6 +48,7 @@ class ThemeStateNotifier extends StateNotifier { historyRetentionPeriod: historyRetentionPeriod, workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, + isDashBotEnabled: isDashBotEnabled, ); await setSettingsToSharedPrefs(state); } diff --git a/lib/screens/common_widgets/envfield_cell.dart b/lib/screens/common_widgets/envfield_cell.dart index ececb8a2..ef9e603b 100644 --- a/lib/screens/common_widgets/envfield_cell.dart +++ b/lib/screens/common_widgets/envfield_cell.dart @@ -1,3 +1,4 @@ +import 'package:apidash/consts.dart'; 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'; @@ -37,6 +38,8 @@ class EnvCellField extends StatelessWidget { decoration: getTextFieldInputDecoration( clrScheme, hintText: hintText, + isDense: true, + contentPadding: kIsMobile ? kPh6b12 : null, ), autocompleteNoTrigger: autocompleteNoTrigger, onChanged: onChanged, diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index 5931c7fe..8955dd3e 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -17,7 +17,7 @@ class Dashboard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final railIdx = ref.watch(navRailIndexStateProvider); - final isDashBotVisible = ref.watch(dashBotVisibilityProvider); + final settings = ref.watch(settingsProvider); return Scaffold( body: SafeArea( child: Stack( @@ -138,8 +138,9 @@ class Dashboard extends ConsumerWidget { ], ), ), - // TODO: Release DashBot - floatingActionButton: !isDashBotVisible ? const DashBotFAB() : null, + floatingActionButton: settings.isDashBotEnabled + ? const DashBotFAB() + : null, ); } } diff --git a/lib/screens/envvar/editor_pane/variables_pane.dart b/lib/screens/envvar/editor_pane/variables_pane.dart index af3a1e39..f6172ec7 100644 --- a/lib/screens/envvar/editor_pane/variables_pane.dart +++ b/lib/screens/envvar/editor_pane/variables_pane.dart @@ -182,7 +182,7 @@ class EditEnvironmentVariablesState color: Theme.of(context).colorScheme.surface, borderRadius: kBorderRadius12, ), - margin: kP10, + margin: kPh10t10, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -203,27 +203,28 @@ class EditEnvironmentVariablesState ), ), ), - kVSpacer40, + if (!kIsMobile) kVSpacer40, ], ), ), - Align( - alignment: Alignment.bottomCenter, - child: Padding( - padding: kPb15, - child: ElevatedButton.icon( - onPressed: () { - variableRows.add(kEnvironmentVariableEmptyModel); - _onFieldChange(selectedId!); - }, - icon: const Icon(Icons.add), - label: const Text( - kLabelAddVariable, - style: kTextStyleButton, + if (!kIsMobile) + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: kPb15, + child: ElevatedButton.icon( + onPressed: () { + variableRows.add(kEnvironmentVariableEmptyModel); + _onFieldChange(selectedId!); + }, + icon: const Icon(Icons.add), + label: const Text( + kLabelAddVariable, + style: kTextStyleButton, + ), ), ), ), - ), ], ); } diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_form_data.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_form_data.dart index 56d3d138..c007cc73 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_form_data.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_form_data.dart @@ -185,7 +185,7 @@ class _FormDataBodyState extends ConsumerState { return Stack( children: [ Container( - margin: kP10, + margin: kPh10t10, child: Column( children: [ Expanded( @@ -205,27 +205,28 @@ class _FormDataBodyState extends ConsumerState { ), ), ), - kVSpacer40, + if (!kIsMobile) kVSpacer40, ], ), ), - Align( - alignment: Alignment.bottomCenter, - child: Padding( - padding: kPb15, - child: ElevatedButton.icon( - onPressed: () { - formRows.add(kFormDataEmptyModel); - _onFieldChange(); - }, - icon: const Icon(Icons.add), - label: const Text( - kLabelAddFormField, - style: kTextStyleButton, + if (!kIsMobile) + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: kPb15, + child: ElevatedButton.icon( + onPressed: () { + formRows.add(kFormDataEmptyModel); + _onFieldChange(); + }, + icon: const Icon(Icons.add), + label: const Text( + kLabelAddFormField, + style: kTextStyleButton, + ), ), ), ), - ), ], ); } 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 658789a0..435346de 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 @@ -178,7 +178,7 @@ class EditRequestHeadersState extends ConsumerState { return Stack( children: [ Container( - margin: kP10, + margin: kPh10t10, child: Column( children: [ Expanded( @@ -198,28 +198,29 @@ class EditRequestHeadersState extends ConsumerState { ), ), ), - kVSpacer40, + if (!kIsMobile) kVSpacer40, ], ), ), - Align( - alignment: Alignment.bottomCenter, - child: Padding( - padding: kPb15, - child: ElevatedButton.icon( - onPressed: () { - headerRows.add(kNameValueEmptyModel); - isRowEnabledList.add(false); - _onFieldChange(); - }, - icon: const Icon(Icons.add), - label: const Text( - kLabelAddHeader, - style: kTextStyleButton, + if (!kIsMobile) + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: kPb15, + child: ElevatedButton.icon( + onPressed: () { + headerRows.add(kNameValueEmptyModel); + isRowEnabledList.add(false); + _onFieldChange(); + }, + icon: const Icon(Icons.add), + label: const Text( + kLabelAddHeader, + style: kTextStyleButton, + ), ), ), ), - ), ], ); } diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_params.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_params.dart index a583b183..be752217 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_params.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_params.dart @@ -178,7 +178,7 @@ class EditRequestURLParamsState extends ConsumerState { return Stack( children: [ Container( - margin: kP10, + margin: kPh10t10, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -199,28 +199,29 @@ class EditRequestURLParamsState extends ConsumerState { ), ), ), - kVSpacer40, + if (!kIsMobile) kVSpacer40, ], ), ), - Align( - alignment: Alignment.bottomCenter, - child: Padding( - padding: kPb15, - child: ElevatedButton.icon( - onPressed: () { - paramRows.add(kNameValueEmptyModel); - isRowEnabledList.add(false); - _onFieldChange(); - }, - icon: const Icon(Icons.add), - label: const Text( - kLabelAddParam, - style: kTextStyleButton, + if (!kIsMobile) + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: kPb15, + child: ElevatedButton.icon( + onPressed: () { + paramRows.add(kNameValueEmptyModel); + isRowEnabledList.add(false); + _onFieldChange(); + }, + icon: const Icon(Icons.add), + label: const Text( + kLabelAddParam, + style: kTextStyleButton, + ), ), ), ), - ), ], ); } diff --git a/lib/screens/settings_page.dart b/lib/screens/settings_page.dart index 28305cd0..606ef516 100644 --- a/lib/screens/settings_page.dart +++ b/lib/screens/settings_page.dart @@ -50,6 +50,18 @@ class SettingsPage extends ConsumerWidget { ref.read(settingsProvider.notifier).update(isDark: value); }, ), + ADListTile( + type: ListTileType.switchOnOff, + title: 'DashBot', + subtitle: + 'Current selection: ${settings.isDashBotEnabled ? "Enabled" : "Disabled"}', + value: settings.isDashBotEnabled, + onChanged: (bool? value) { + ref + .read(settingsProvider.notifier) + .update(isDashBotEnabled: value); + }, + ), ADListTile( type: ListTileType.switchOnOff, title: 'Collection Pane Scrollbar Visiblity', diff --git a/lib/utils/ui_utils.dart b/lib/utils/ui_utils.dart index 4f72a1a3..68eaa82c 100644 --- a/lib/utils/ui_utils.dart +++ b/lib/utils/ui_utils.dart @@ -51,6 +51,7 @@ Color getHTTPMethodColor(HTTPVerb? method) { HTTPVerb.put => kColorHttpMethodPut, HTTPVerb.patch => kColorHttpMethodPatch, HTTPVerb.delete => kColorHttpMethodDelete, + HTTPVerb.options => kColorHttpMethodOptions, _ => kColorHttpMethodGet, }; return col; diff --git a/lib/widgets/previewer_json.dart b/lib/widgets/previewer_json.dart index 3f8e2b19..811cbbc8 100644 --- a/lib/widgets/previewer_json.dart +++ b/lib/widgets/previewer_json.dart @@ -281,8 +281,18 @@ class _JsonPreviewerState extends State { size: 18, ), onPressed: () async { - await _copy( - kJsonEncoder.convert(toJson(node)), sm); + final val = toJson(node); + String toCopy = ''; + if (node.isClass || + node.isArray || + node.isRoot) { + toCopy = kJsonEncoder.convert(val); + } else { + toCopy = (val.values as Iterable) + .first + .toString(); + } + await _copy(toCopy, sm); }, ), ) diff --git a/packages/apidash_core/lib/consts.dart b/packages/apidash_core/lib/consts.dart index 1d7044c7..c1d45ad4 100644 --- a/packages/apidash_core/lib/consts.dart +++ b/packages/apidash_core/lib/consts.dart @@ -17,7 +17,8 @@ enum HTTPVerb { post("POST"), put("PUT"), patch("PAT"), - delete("DEL"); + delete("DEL"), + options("OPT"); const HTTPVerb(this.abbr); final String abbr; diff --git a/packages/apidash_core/lib/models/http_request_model.g.dart b/packages/apidash_core/lib/models/http_request_model.g.dart index 29005786..13795ded 100644 --- a/packages/apidash_core/lib/models/http_request_model.g.dart +++ b/packages/apidash_core/lib/models/http_request_model.g.dart @@ -58,6 +58,7 @@ const _$HTTPVerbEnumMap = { HTTPVerb.put: 'put', HTTPVerb.patch: 'patch', HTTPVerb.delete: 'delete', + HTTPVerb.options: 'options', }; const _$ContentTypeEnumMap = { diff --git a/packages/apidash_core/lib/services/http_service.dart b/packages/apidash_core/lib/services/http_service.dart index dbd93086..1b05976a 100644 --- a/packages/apidash_core/lib/services/http_service.dart +++ b/packages/apidash_core/lib/services/http_service.dart @@ -94,6 +94,7 @@ Future<(HttpResponse?, Duration?, String?)> sendHttpRequest( case HTTPVerb.put: case HTTPVerb.patch: case HTTPVerb.delete: + case HTTPVerb.options: final request = prepareHttpRequest( url: requestUrl, method: requestModel.method.name.toUpperCase(), diff --git a/packages/apidash_design_system/lib/tokens/colors.dart b/packages/apidash_design_system/lib/tokens/colors.dart index 11943bd3..7b203212 100644 --- a/packages/apidash_design_system/lib/tokens/colors.dart +++ b/packages/apidash_design_system/lib/tokens/colors.dart @@ -23,6 +23,7 @@ final kColorHttpMethodPost = Colors.blue.shade800; final kColorHttpMethodPut = Colors.amber.shade900; final kColorHttpMethodPatch = kColorHttpMethodPut; final kColorHttpMethodDelete = Colors.red.shade800; +final kColorHttpMethodOptions = Colors.deepPurple.shade800; final kColorGQL = Colors.pink.shade600; diff --git a/packages/apidash_design_system/lib/tokens/measurements.dart b/packages/apidash_design_system/lib/tokens/measurements.dart index f760f8f9..b06a9ce9 100644 --- a/packages/apidash_design_system/lib/tokens/measurements.dart +++ b/packages/apidash_design_system/lib/tokens/measurements.dart @@ -53,12 +53,22 @@ const kPh20t40 = EdgeInsets.only( right: 20, top: 40, ); +const kPh10t10 = EdgeInsets.only( + left: 10, + right: 10, + top: 10, +); const kPs0o6 = EdgeInsets.only( left: 0, top: 6, right: 6, bottom: 6, ); +const kPh6b12 = EdgeInsets.only( + left: 6.0, + right: 6.0, + bottom: 12.0, +); const kPh60 = EdgeInsets.symmetric(horizontal: 60); const kPh60v60 = EdgeInsets.symmetric(vertical: 60, horizontal: 60); const kPt24l4 = EdgeInsets.only( diff --git a/test/models/http_request_models.dart b/test/models/http_request_models.dart index 30260983..10b84eda 100644 --- a/test/models/http_request_models.dart +++ b/test/models/http_request_models.dart @@ -466,3 +466,9 @@ const httpRequestModelPost13 = HttpRequestModel( "text": "I LOVE Flutter" }""", ); + +/// Basic OPTIONS request model +const httpRequestModelOptions1 = HttpRequestModel( + method: HTTPVerb.options, + url: 'https://reqbin.com/echo/options', +); diff --git a/test/models/request_models.dart b/test/models/request_models.dart index 9a6a91c7..74d11376 100644 --- a/test/models/request_models.dart +++ b/test/models/request_models.dart @@ -253,3 +253,9 @@ const requestModelPost13 = RequestModel( apiType: APIType.rest, httpRequestModel: httpRequestModelPost13, ); + +const requestModelOptions1 = RequestModel( + id: 'options1', + apiType: APIType.rest, + httpRequestModel: httpRequestModelOptions1, +); diff --git a/test/models/response_model_test.dart b/test/models/response_model_test.dart index 0060b13b..74872cdb 100644 --- a/test/models/response_model_test.dart +++ b/test/models/response_model_test.dart @@ -119,4 +119,21 @@ void main() { test('Testing hashcode', () { expect(responseModel.hashCode, greaterThan(0)); }); + + test('Testing fromResponse for OPTIONS method', () async { + var responseRec = await sendHttpRequest( + requestModelOptions1.id, + requestModelOptions1.apiType, + requestModelOptions1.httpRequestModel!, + defaultUriScheme: kDefaultUriScheme, + noSSL: false, + ); + + final responseData = responseModel.fromResponse(response: responseRec.$1!); + expect(responseData.statusCode, 200); + expect(responseData.headers?['access-control-allow-methods'], 'GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS'); + expect(responseData.headers?['access-control-allow-methods']?.contains("OPTIONS"), true); + expect(responseData.headers?['allow'], 'GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS'); + expect(responseData.headers?['allow']?.contains("OPTIONS"), true); + }); }