diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 04873dba..5bdb6ff8 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -9,6 +9,7 @@ _Add your description_ ### Checklist - [ ] I have gone through the [contributing guide](https://github.com/foss42/apidash/blob/main/CONTRIBUTING.md) +- [ ] I have updated my branch and synced it with project `main` branch before making this PR - [ ] I have run the tests (`flutter test`) and all tests are passing ## Added/updated tests? diff --git a/lib/codegen/others/curl.dart b/lib/codegen/others/curl.dart index 2ba59765..2f177609 100644 --- a/lib/codegen/others/curl.dart +++ b/lib/codegen/others/curl.dart @@ -58,7 +58,7 @@ class cURLCodeGen { } else if (requestModel.hasFormData) { for (var formData in requestModel.formDataList) { var templateFormData = jj.Template(kTemplateFormData); - if (formData.name.isNotEmpty && formData.value.isNotEmpty) { + if (formData.name.isNotEmpty) { result += templateFormData.render({ "name": formData.name, "value": diff --git a/lib/models/request_model.dart b/lib/models/request_model.dart index 6bdff603..0ea6d735 100644 --- a/lib/models/request_model.dart +++ b/lib/models/request_model.dart @@ -80,10 +80,10 @@ class RequestModel { bool get hasFormData => kMethodsWithBody.contains(method) && hasFormDataContentType && - (requestFormDataList ?? []).isNotEmpty; + formDataMapList.isNotEmpty; List get formDataList => requestFormDataList ?? []; - List> get formDataMapList => + List> get formDataMapList => rowsToFormDataMapList(requestFormDataList) ?? []; bool get hasFileInFormData => formDataList .map((e) => e.type == FormDataType.file) @@ -94,12 +94,13 @@ class RequestModel { RequestModel duplicate({ required String id, + String? name, }) { return RequestModel( id: id, method: method, url: url, - name: "$name (copy)", + name: name ?? "${this.name} (copy)", description: description, requestHeaders: requestHeaders != null ? [...requestHeaders!] : null, requestParams: requestParams != null ? [...requestParams!] : null, @@ -137,6 +138,7 @@ class RequestModel { var params = requestParams ?? this.requestParams; var enabledHeaders = isHeaderEnabledList ?? this.isHeaderEnabledList; var enabledParams = isParamEnabledList ?? this.isParamEnabledList; + var formDataList = requestFormDataList ?? this.requestFormDataList; return RequestModel( id: id ?? this.id, method: method ?? this.method, @@ -151,7 +153,7 @@ class RequestModel { requestBodyContentType: requestBodyContentType ?? this.requestBodyContentType, requestBody: requestBody ?? this.requestBody, - requestFormDataList: requestFormDataList ?? this.requestFormDataList, + requestFormDataList: formDataList != null ? [...formDataList] : null, responseStatus: responseStatus ?? this.responseStatus, message: message ?? this.message, responseModel: responseModel ?? this.responseModel, diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 4ecc51ab..db6b7cf3 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -96,6 +96,18 @@ class CollectionStateNotifier state = map; } + void clearResponse(String? id) { + if (id == null || state?[id] == null) return; + var currentModel = state![id]!; + final newModel = currentModel.duplicate( + id: id, + name: currentModel.name, + ); + var map = {...state!}; + map[id] = newModel; + state = map; + } + void duplicate(String id) { final newId = getNewUuid(); @@ -134,21 +146,22 @@ class CollectionStateNotifier ResponseModel? responseModel, }) { final newModel = state![id]!.copyWith( - method: method, - url: url, - name: name, - description: description, - requestTabIndex: requestTabIndex, - requestHeaders: requestHeaders, - requestParams: requestParams, - isHeaderEnabledList: isHeaderEnabledList, - isParamEnabledList: isParamEnabledList, - requestBodyContentType: requestBodyContentType, - requestBody: requestBody, - requestFormDataList: requestFormDataList, - responseStatus: responseStatus, - message: message, - responseModel: responseModel); + method: method, + url: url, + name: name, + description: description, + requestTabIndex: requestTabIndex, + requestHeaders: requestHeaders, + requestParams: requestParams, + isHeaderEnabledList: isHeaderEnabledList, + isParamEnabledList: isParamEnabledList, + requestBodyContentType: requestBodyContentType, + requestBody: requestBody, + requestFormDataList: requestFormDataList, + responseStatus: responseStatus, + message: message, + responseModel: responseModel, + ); //print(newModel); var map = {...state!}; map[id] = newModel; diff --git a/lib/screens/home_page/editor_pane/details_card/response_pane.dart b/lib/screens/home_page/editor_pane/details_card/response_pane.dart index 548d17a8..703b873d 100644 --- a/lib/screens/home_page/editor_pane/details_card/response_pane.dart +++ b/lib/screens/home_page/editor_pane/details_card/response_pane.dart @@ -34,6 +34,7 @@ class ResponseDetails extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + var sm = ScaffoldMessenger.of(context); final responseStatus = ref.watch( selectedRequestModelProvider.select((value) => value?.responseStatus)); final message = ref @@ -46,6 +47,14 @@ class ResponseDetails extends ConsumerWidget { responseStatus: responseStatus, message: message, time: responseModel?.time, + onClearResponse: () { + final selectedRequest = ref.read(selectedRequestModelProvider); + ref + .read(collectionStateNotifierProvider.notifier) + .clearResponse(selectedRequest?.id); + sm.hideCurrentSnackBar(); + sm.showSnackBar(getSnackBar('Response cleared')); + }, ), const Expanded( child: ResponseTabs(), diff --git a/lib/screens/settings_page.dart b/lib/screens/settings_page.dart index fde7dce7..8bdf6580 100644 --- a/lib/screens/settings_page.dart +++ b/lib/screens/settings_page.dart @@ -66,39 +66,68 @@ class SettingsPage extends ConsumerWidget { title: const Text('Default URI Scheme'), subtitle: Text( '$kDefaultUri → ${settings.defaultUriScheme}://$kDefaultUri'), - trailing: DropdownMenu( - onSelected: (value) { - ref - .read(settingsProvider.notifier) - .update(defaultUriScheme: value); - }, - initialSelection: settings.defaultUriScheme, - dropdownMenuEntries: kSupportedUriSchemes - .map>((value) { - return DropdownMenuEntry( - value: value, - label: value, - ); - }).toList()), + trailing: Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.onSurface, + ), + borderRadius: kBorderRadius8, + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + borderRadius: kBorderRadius8, + onChanged: (value) { + ref + .read(settingsProvider.notifier) + .update(defaultUriScheme: value); + }, + value: settings.defaultUriScheme, + items: kSupportedUriSchemes + .map>((String value) { + return DropdownMenuItem( + value: value, + child: Padding( + padding: kP10, + child: Text(value), + ), + ); + }).toList(), + ), + ), + ), ), ListTile( contentPadding: kPb10, hoverColor: kColorTransparent, title: const Text('Default Code Generator'), - trailing: DropdownMenu( - onSelected: (value) { - ref - .read(settingsProvider.notifier) - .update(defaultCodeGenLang: value); - }, - initialSelection: settings.defaultCodeGenLang, - dropdownMenuEntries: CodegenLanguage.values - .map>((value) { - return DropdownMenuEntry( - value: value, - label: value.label, - ); - }).toList()), + trailing: Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.onSurface, + ), + borderRadius: kBorderRadius8, + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + borderRadius: kBorderRadius8, + value: settings.defaultCodeGenLang, + onChanged: (value) { + ref + .read(settingsProvider.notifier) + .update(defaultCodeGenLang: value); + }, + items: CodegenLanguage.values.map((value) { + return DropdownMenuItem( + value: value, + child: Padding( + padding: kP10, + child: Text(value.label), + ), + ); + }).toList(), + ), + ), + ), ), CheckboxListTile( contentPadding: EdgeInsets.zero, diff --git a/lib/utils/convert_utils.dart b/lib/utils/convert_utils.dart index bc704af9..e855568b 100644 --- a/lib/utils/convert_utils.dart +++ b/lib/utils/convert_utils.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; import 'dart:convert'; +import 'package:collection/collection.dart'; import '../models/models.dart'; import '../consts.dart'; import 'package:http/http.dart' as http; @@ -90,18 +91,22 @@ List? mapToRows(Map? kvMap) { return finalRows; } -List>? rowsToFormDataMapList( +List>? rowsToFormDataMapList( List? kvRows, ) { if (kvRows == null) { return null; } - List> finalMap = kvRows - .map((FormDataModel formData) => { - "name": formData.name, - "value": formData.value, - "type": formData.type.name, - }) + List> finalMap = kvRows + .map((FormDataModel formData) => + (formData.name.trim().isEmpty && formData.value.trim().isEmpty) + ? null + : { + "name": formData.name, + "value": formData.value, + "type": formData.type.name, + }) + .whereNotNull() .toList(); return finalMap; } diff --git a/lib/utils/file_utils.dart b/lib/utils/file_utils.dart index 68554153..58d69513 100644 --- a/lib/utils/file_utils.dart +++ b/lib/utils/file_utils.dart @@ -50,7 +50,7 @@ String getShortPath(String path) { String getFilenameFromPath(String path) { var f = p.split(path); - return f.last; + return f.lastOrNull ?? ""; } String getTempFileName() { diff --git a/lib/widgets/buttons.dart b/lib/widgets/buttons.dart index f9baf0c4..f4280380 100644 --- a/lib/widgets/buttons.dart +++ b/lib/widgets/buttons.dart @@ -235,3 +235,27 @@ class SaveButton extends StatelessWidget { ); } } + +class ClearResponseButton extends StatelessWidget { + const ClearResponseButton({ + super.key, + this.onPressed, + }); + + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return Tooltip( + message: 'Clear response', + child: TextButton( + style: TextButton.styleFrom(minimumSize: const Size(40, 40)), + onPressed: onPressed, + child: const Icon( + Icons.delete, + size: 20, + ), + ), + ); + } +} diff --git a/lib/widgets/response_widgets.dart b/lib/widgets/response_widgets.dart index 539a2a34..adf74985 100644 --- a/lib/widgets/response_widgets.dart +++ b/lib/widgets/response_widgets.dart @@ -55,11 +55,13 @@ class ResponsePaneHeader extends StatelessWidget { this.responseStatus, this.message, this.time, + this.onClearResponse, }); final int? responseStatus; final String? message; final Duration? time; + final VoidCallback? onClearResponse; @override Widget build(BuildContext context) { @@ -116,6 +118,10 @@ class ResponsePaneHeader extends StatelessWidget { color: Theme.of(context).colorScheme.secondary, ), ), + kHSpacer10, + ClearResponseButton( + onPressed: onClearResponse, + ) ], ), ), diff --git a/test/widgets/buttons_test.dart b/test/widgets/buttons_test.dart index 2b961e07..71492c91 100644 --- a/test/widgets/buttons_test.dart +++ b/test/widgets/buttons_test.dart @@ -186,4 +186,18 @@ void main() { expect(find.byIcon(Icons.save), findsOneWidget); expect(find.text("Save"), findsOneWidget); }); + + testWidgets('Testing for ClearResponseButton', (tester) async { + await tester.pumpWidget( + MaterialApp( + title: 'ClearResponseButton', + theme: kThemeDataLight, + home: const Scaffold( + body: ClearResponseButton(), + ), + ), + ); + + expect(find.byIcon(Icons.delete), findsOneWidget); + }); }