diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 721b55e6..04873dba 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -16,4 +16,3 @@ _We encourage you to add relevant test cases._ - [ ] Yes - [ ] No, and this is why: _please replace this line with details on why tests have not been included_ -- [ ] I need help with writing tests diff --git a/README.md b/README.md index 15dc46e1..d8b258e5 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ Here is the complete list of mimetypes that can be directly previewed in API Das | File Type | Mimetype | Extension | Comment | | --------- | -------------------------- | ----------------- | -------- | | PDF | `application/pdf` | `.pdf` | | +| CSV | `text/csv` | `.csv` | Can be improved | | Image | `image/apng` | `.apng` | Animated | | Image | `image/avif` | `.avif` | | | Image | `image/bmp` | `.bmp` | | @@ -177,14 +178,14 @@ Here is the complete list of mimetypes that are syntax highlighted in API Dash: | ------------------ | --------- | ------------------------------------------------------------------------------------------------------------------ | | `application/json` | `.json` | Other mimetypes like `application/geo+json`, `application/vcard+json` that are based on `json` are also supported. | | `application/xml` | `.xml` | Other mimetypes like `application/xhtml+xml`, `application/vcard+xml` that are based on `xml` are also supported. | -| `text/xml` | `.xml` | | -| `application/yaml` | `.yaml` | Others - `application/x-yaml` or `application/x-yml` | -| `text/yaml` | `.yaml` | Others - `text/yml` | -| `application/sql` | `.sql` | | -| `text/css` | `.css` | | -| `text/html` | `.html` | Only syntax highlighting, no web preview. | -| `text/javascript` | `.js` | | -| `text/markdown` | `.md` | | +| `text/xml` | `.xml` | | +| `application/yaml` | `.yaml` | Others - `application/x-yaml` or `application/x-yml` | +| `text/yaml` | `.yaml` | Others - `text/yml` | +| `application/sql` | `.sql` | | +| `text/css` | `.css` | | +| `text/html` | `.html` | Only syntax highlighting, no web preview. | +| `text/javascript` | `.js` | | +| `text/markdown` | `.md` | | ## What's new in v0.3.0? diff --git a/analysis_options.yaml b/analysis_options.yaml index 1d5eafa4..9a1eabb4 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -3,8 +3,9 @@ include: package:flutter_lints/flutter.yaml analyzer: errors: invalid_annotation_target: ignore - enable-experiment: - - records + exclude: + - "**/*.freezed.dart" + - "**/*.g.dart" linter: rules: diff --git a/lib/consts.dart b/lib/consts.dart index b430794e..aa7e8aa4 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -379,6 +379,7 @@ const Map>> kSubTypeDefaultViewOptions: kRawBodyViewOptions, kSubTypeCss: kCodeRawBodyViewOptions, kSubTypeHtml: kCodeRawBodyViewOptions, + kSubTypeCsv: kPreviewRawBodyViewOptions, kSubTypeJavascript: kCodeRawBodyViewOptions, kSubTypeMarkdown: kCodeRawBodyViewOptions, kSubTypeTextXml: kCodeRawBodyViewOptions, @@ -496,6 +497,9 @@ const kAudioError = const kRaiseIssue = "\nPlease raise an issue in API Dash GitHub repo so that we can resolve it."; +const kCsvError = + "There seems to be an issue rendering this CSV. Please raise an issue in API Dash GitHub repo so that we can resolve it."; + const kHintTextUrlCard = "Enter API endpoint like api.foss42.com/country/codes"; const kLabelPlusNew = "+ New"; const kLabelSend = "Send"; diff --git a/lib/utils/header_utils.dart b/lib/utils/header_utils.dart index 45a24fe9..1450cc01 100644 --- a/lib/utils/header_utils.dart +++ b/lib/utils/header_utils.dart @@ -2,6 +2,7 @@ Map headers = { "Accept": "Specifies the media types that are acceptable for the response.", "Accept-Encoding": "Indicates the encoding methods the client can understand.", + "Accept-Charset": "Specifies the character sets that are acceptable.", "Access-Control-Allow-Headers": "Specifies a list of HTTP headers that can be used in an actual request after a preflight request including the Access-Control-Request-Headers header is made.", "Access-Control-Allow-Methods": @@ -41,11 +42,16 @@ Map headers = { "Cross-Origin-Resource-Policy": "Controls how cross-origin requests for resources are handled.", "Date": "Indicates the date and time at which the message was sent.", + "Device-Memory": + "Indicates the approximate amount of device memory in gigabytes.", "DNT": "Informs websites whether the user's preference is to opt out of online tracking.", "Expect": "Indicates certain expectations that need to be met by the server.", "Expires": "Contains the date/time after which the response is considered expired", + "Forwarded": + "Contains information from the client-facing side of proxy servers that is altered or lost when a proxy is involved in the path of the request.", + "From": "Contains an Internet email address for a human user who controls the requesting user agent.", "Host": "Specifies the domain name of the server and the port number.", "If-Match": "Used for conditional requests, allows the server to respond based on certain conditions.", @@ -57,9 +63,15 @@ Map headers = { "Used in conjunction with the Range header to conditionally request a partial resource.", "If-Unmodified-Since": "Used for conditional requests, allows the server to respond based on certain conditions.", + "Keep-Alive": + "Used to allow the connection to be reused for further requests.", "Location": "Indicates the URL a client should redirect to for further interaction.", + "Max-Forwards": + "Indicates the remaining number of times a request can be forwarded by proxies.", "Origin": "Specifies the origin of a cross-origin request.", + "Proxy-Authorization": + "Contains credentials for authenticating a client with a proxy server.", "Range": "Used to request only part of a resource, typically in the context of downloading large files.", "Referer": @@ -68,10 +80,14 @@ Map headers = { "Specifies how much information the browser should include in the Referer header when navigating to other pages.", "Retry-After": "Informs the client how long it should wait before making another request after a server has responded with a rate-limiting status code.", + "Save-Data": + "Indicates the client's preference for reduced data usage.", "Server": "Indicates the software used by the origin server.", "Strict-Transport-Security": "Instructs the browser to always use HTTPS for the given domain.", "TE": "Specifies the transfer encodings that are acceptable to the client.", + "Upgrade-Insecure-Requests": + "Instructs the browser to prefer secure connections when available.", "User-Agent": "Identifies the client software and version making the request.", "Via": diff --git a/lib/widgets/csv_previewer.dart b/lib/widgets/csv_previewer.dart new file mode 100644 index 00000000..17e8dd48 --- /dev/null +++ b/lib/widgets/csv_previewer.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:csv/csv.dart'; +import 'error_message.dart'; +import '../consts.dart'; + +class CsvPreviewer extends StatelessWidget { + const CsvPreviewer({super.key, required this.body}); + + final String body; + + @override + Widget build(BuildContext context) { + try { + final List> csvData = + const CsvToListConverter().convert(body, eol: '\n'); + return SingleChildScrollView( + scrollDirection: Axis.vertical, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: DataTable( + columns: csvData[0] + .map( + (item) => DataColumn( + label: Text( + item.toString(), + ), + ), + ) + .toList(), + rows: csvData + .skip(1) + .map( + (csvrow) => DataRow( + cells: csvrow + .map( + (csvItem) => DataCell( + Text( + csvItem.toString(), + ), + ), + ) + .toList(), + ), + ) + .toList(), + ), + ), + ); + } catch (e) { + return const ErrorMessage(message: kCsvError); + } + } +} diff --git a/lib/widgets/headerfield.dart b/lib/widgets/headerfield.dart index 9c678ee3..5bce6c8f 100644 --- a/lib/widgets/headerfield.dart +++ b/lib/widgets/headerfield.dart @@ -1,6 +1,6 @@ +import 'package:apidash/consts.dart'; import 'package:apidash/utils/header_utils.dart'; import 'package:flutter/material.dart'; -import 'package:apidash/consts.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; class HeaderField extends StatefulWidget { @@ -41,6 +41,7 @@ class _HeaderFieldState extends State { @override void didUpdateWidget(HeaderField oldWidget) { + super.didUpdateWidget(oldWidget); if (oldWidget.initialValue != widget.initialValue) { controller.text = widget.initialValue ?? ""; controller.selection = @@ -54,8 +55,8 @@ class _HeaderFieldState extends State { return TypeAheadField( key: Key(widget.keyId), hideOnEmpty: true, - minCharsForSuggestions: 1, - onSuggestionSelected: (value) { + controller: controller, + onSelected: (value) { setState(() { controller.text = value; }); @@ -68,19 +69,17 @@ class _HeaderFieldState extends State { ); }, suggestionsCallback: headerSuggestionCallback, - suggestionsBoxDecoration: suggestionBoxDecorations(context), - textFieldConfiguration: TextFieldConfiguration( + decorationBuilder: (context, child) => + suggestionBoxDecorations(context, child, colorScheme), + constraints: const BoxConstraints(maxHeight: 400), + builder: (context, controller, focusNode) => TextField( onChanged: widget.onChanged, controller: controller, - style: kCodeStyle.copyWith( - color: colorScheme.onSurface, - ), + focusNode: focusNode, + style: kCodeStyle.copyWith(color: colorScheme.onSurface), decoration: InputDecoration( hintStyle: kCodeStyle.copyWith( - color: colorScheme.outline.withOpacity( - kHintOpacity, - ), - ), + color: colorScheme.outline.withOpacity(kHintOpacity)), hintText: widget.hintText, focusedBorder: UnderlineInputBorder( borderSide: BorderSide( @@ -99,22 +98,26 @@ class _HeaderFieldState extends State { ); } - SuggestionsBoxDecoration suggestionBoxDecorations(BuildContext context) { - return SuggestionsBoxDecoration( - elevation: 4, - constraints: const BoxConstraints(maxHeight: 400), - shape: RoundedRectangleBorder( - side: BorderSide( - color: Theme.of(context).dividerColor, - width: 1.2, + Theme suggestionBoxDecorations( + BuildContext context, Widget child, ColorScheme colorScheme) { + return Theme( + data: ThemeData(colorScheme: colorScheme), + child: Material( + elevation: 4, + shape: RoundedRectangleBorder( + side: BorderSide(color: Theme.of(context).dividerColor, width: 1.2), + borderRadius: const BorderRadius.vertical(bottom: Radius.circular(8)), ), - borderRadius: const BorderRadius.vertical(bottom: Radius.circular(8)), + clipBehavior: Clip.hardEdge, + child: child, ), - clipBehavior: Clip.hardEdge, ); } - Future> headerSuggestionCallback(String pattern) async { + Future?> headerSuggestionCallback(String pattern) async { + if (pattern.isEmpty) { + return null; + } return getHeaderSuggestions(pattern); } } diff --git a/lib/widgets/json_previewer.dart b/lib/widgets/json_previewer.dart index 078a9721..46d066bf 100644 --- a/lib/widgets/json_previewer.dart +++ b/lib/widgets/json_previewer.dart @@ -154,6 +154,7 @@ class _JsonPreviewerState extends State { @override void didUpdateWidget(JsonPreviewer oldWidget) { + super.didUpdateWidget(oldWidget); if (oldWidget.code != widget.code) { store.buildNodes(widget.code, areAllCollapsed: true); store.expandAll(); diff --git a/lib/widgets/previewer.dart b/lib/widgets/previewer.dart index 9841ac5b..dd2f5186 100644 --- a/lib/widgets/previewer.dart +++ b/lib/widgets/previewer.dart @@ -7,6 +7,7 @@ import 'package:vector_graphics_compiler/vector_graphics_compiler.dart'; import 'error_message.dart'; import 'uint8_audio_player.dart'; import 'json_previewer.dart'; +import 'csv_previewer.dart'; import '../consts.dart'; class Previewer extends StatefulWidget { @@ -81,6 +82,9 @@ class _PreviewerState extends State { }, ); } + if (widget.type == kTypeText && widget.subtype == kSubTypeCsv) { + return CsvPreviewer(body: widget.body); + } if (widget.type == kTypeVideo) { // TODO: Video Player } diff --git a/pubspec.lock b/pubspec.lock index 2f3f7f55..f48a9105 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -217,6 +217,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + csv: + dependency: "direct main" + description: + name: csv + sha256: "63ed2871dd6471193dffc52c0e6c76fb86269c00244d244297abbb355c84a86e" + url: "https://pub.dev" + source: hosted + version: "5.1.1" dart_style: dependency: "direct main" description: @@ -306,10 +314,10 @@ packages: dependency: transitive description: name: flutter_keyboard_visibility - sha256: "4983655c26ab5b959252ee204c2fffa4afeb4413cd030455194ec0caa3b8e7cb" + sha256: "98664be7be0e3ffca00de50f7f6a287ab62c763fc8c762e0a21584584a3ff4f8" url: "https://pub.dev" source: hosted - version: "5.4.1" + version: "6.0.0" flutter_keyboard_visibility_linux: dependency: transitive description: @@ -407,10 +415,10 @@ packages: dependency: "direct main" description: name: flutter_typeahead - sha256: b9942bd5b7611a6ec3f0730c477146cffa4cd4b051077983ba67ddfc9e7ee818 + sha256: d64712c65db240b1057559b952398ebb6e498077baeebf9b0731dade62438a6d url: "https://pub.dev" source: hosted - version: "4.8.0" + version: "5.2.0" flutter_web_plugins: dependency: transitive description: flutter @@ -625,6 +633,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" lints: dependency: transitive description: @@ -661,26 +693,26 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" mime: dependency: transitive description: @@ -757,10 +789,10 @@ packages: dependency: "direct main" description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_parsing: dependency: transitive description: @@ -853,10 +885,34 @@ packages: dependency: transitive description: name: pointer_interceptor - sha256: adf7a637f97c077041d36801b43be08559fd4322d2127b3f20bb7be1b9eebc22 + sha256: bd18321519718678d5fa98ad3a3359cbc7a31f018554eab80b73d08a7f0c165a url: "https://pub.dev" source: hosted - version: "0.9.3+7" + version: "0.10.1" + pointer_interceptor_ios: + dependency: transitive + description: + name: pointer_interceptor_ios + sha256: "2e73c39452830adc4695757130676a39412a3b7f3c34e3f752791b5384770877" + url: "https://pub.dev" + source: hosted + version: "0.10.0+2" + 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: "9386e064097fd16419e935c23f08f35b58e6aaec155dd39bd6a003b88f9c14b4" + url: "https://pub.dev" + source: hosted + version: "0.10.1+2" pointycastle: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4196d007..8441a0f5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,7 +41,7 @@ dependencies: json_annotation: ^4.8.1 printing: ^5.11.1 package_info_plus: ^4.1.0 - flutter_typeahead: ^4.8.0 + flutter_typeahead: ^5.2.0 provider: ^6.0.5 json_data_explorer: git: @@ -54,6 +54,7 @@ dependencies: code_builder: ^4.9.0 dart_style: ^2.3.4 json_text_field: ^1.1.0 + csv: ^5.1.1 dev_dependencies: flutter_test: diff --git a/test/utils/header_utils_test.dart b/test/utils/header_utils_test.dart index 73c15721..139ba553 100644 --- a/test/utils/header_utils_test.dart +++ b/test/utils/header_utils_test.dart @@ -115,6 +115,7 @@ void main() { String pattern = "x-"; List expected = [ "Access-Control-Max-Age", + "Max-Forwards", "X-Api-Key", "X-Content-Type-Options", "X-CSRF-Token", diff --git a/test/widget_test.dart b/test/widget_test.dart index 48eb1d93..3f806c72 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,3 +1,6 @@ +// ignore_for_file: unused_import +// TODO: Added ignore to calculate code coverage + import 'package:apidash/main.dart'; import 'package:apidash/app.dart'; import 'package:apidash/common/utils.dart'; diff --git a/test/widgets/intro_message_test.dart b/test/widgets/intro_message_test.dart index 798afad8..4e256c82 100644 --- a/test/widgets/intro_message_test.dart +++ b/test/widgets/intro_message_test.dart @@ -1,9 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:apidash/widgets/intro_message.dart'; +import 'package:package_info_plus/package_info_plus.dart'; void main() { testWidgets('Testing Intro Message', (tester) async { + PackageInfo.setMockInitialValues( + appName: 'API Dash', + packageName: 'dev.apidash.apidash', + version: '1.0.0', + buildNumber: '1', + buildSignature: 'buildSignature'); await tester.pumpWidget( const MaterialApp( title: 'Intro Message', @@ -13,7 +20,7 @@ void main() { ), ); - await tester.pumpAndSettle(); + await tester.pump(); expect(find.text('Welcome to API Dash ⚡️'), findsOneWidget); expect(find.byType(RichText), findsAtLeastNWidgets(1)); @@ -25,5 +32,5 @@ void main() { expect(find.byIcon(Icons.star), findsOneWidget); expect(find.text('Star on GitHub'), findsOneWidget); await tester.tap(find.byIcon(Icons.star)); - }, skip: true); + }); } diff --git a/test/widgets/previewer_test.dart b/test/widgets/previewer_test.dart index 0d2f68cd..c26793bb 100644 --- a/test/widgets/previewer_test.dart +++ b/test/widgets/previewer_test.dart @@ -231,4 +231,26 @@ void main() { await tester.pumpAndSettle(); expect(find.text(kSvgError), findsOneWidget); }); + + testWidgets('Testing when type/subtype is text/csv', (tester) async { + String csvDataString = + 'Id,Name,Age\n1,John Doe,40\n2,Dbestech,41\n3,Voldermort,71\n4,Joe Biden,80\n5,Ryo Hanamura,35'; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Previewer( + type: kTypeText, + subtype: kSubTypeCsv, + bytes: Uint8List.fromList([]), + body: csvDataString, + ), + ), + ), + ); + + expect(find.byType(DataTable), findsOneWidget); + expect(find.text('John Doe'), findsOneWidget); + expect(find.text('41'), findsOneWidget); + }); }