From cb3da14188c46f455fc113cef42f94a8f082b5d0 Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Sat, 19 Apr 2025 13:29:22 +0530 Subject: [PATCH 01/25] feat(explorer): initialize API Explorer layout with header and search functionality - Added ExplorerPage with structured layout - Implemented ExplorerHeader with title and import button - Created ExplorerBody containing a responsive ApiSearchBar and placeholder content - Built reusable ApiSearchBar widget with clear and change handlers - Set up modular file structure for future API Explorer development Signed-off-by: Balasubramaniam12007 --- lib/screens/dashboard.dart | 19 ++++- .../common_widgets/api_search_bar.dart | 72 +++++++++++++++++++ lib/screens/explorer/explorer_body.dart | 43 +++++++++++ lib/screens/explorer/explorer_header.dart | 24 +++++++ lib/screens/explorer/explorer_page.dart | 24 +++++++ 5 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 lib/screens/explorer/common_widgets/api_search_bar.dart create mode 100644 lib/screens/explorer/explorer_body.dart create mode 100644 lib/screens/explorer/explorer_header.dart create mode 100644 lib/screens/explorer/explorer_page.dart diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index 428ffaebc..2127b53ef 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -9,6 +9,7 @@ import 'common_widgets/common_widgets.dart'; import 'envvar/environment_page.dart'; import 'home_page/home_page.dart'; import 'history/history_page.dart'; +import 'explorer/explorer_page.dart'; import 'settings_page.dart'; class Dashboard extends ConsumerWidget { @@ -68,6 +69,19 @@ class Dashboard extends ConsumerWidget { 'History', style: Theme.of(context).textTheme.labelSmall, ), + kVSpacer10, + IconButton( + isSelected: railIdx == 3, + onPressed: () { + ref.read(navRailIndexStateProvider.notifier).state = 3; + }, + icon: const Icon(Icons.explore_outlined), + selectedIcon: const Icon(Icons.explore), + ), + Text( + 'Explorer', + style: Theme.of(context).textTheme.labelSmall, + ), ], ), Expanded( @@ -92,7 +106,7 @@ class Dashboard extends ConsumerWidget { padding: const EdgeInsets.only(bottom: 16.0), child: NavbarButton( railIdx: railIdx, - buttonIdx: 3, + buttonIdx: 4, selectedIcon: Icons.settings, icon: Icons.settings_outlined, label: 'Settings', @@ -118,7 +132,8 @@ class Dashboard extends ConsumerWidget { HomePage(), EnvironmentPage(), HistoryPage(), - SettingsPage(), + ExplorerPage(), // Added ExplorerPage at index 3 + SettingsPage(), // Shifted to index 4 ], ), ) diff --git a/lib/screens/explorer/common_widgets/api_search_bar.dart b/lib/screens/explorer/common_widgets/api_search_bar.dart new file mode 100644 index 000000000..d39dfe3d8 --- /dev/null +++ b/lib/screens/explorer/common_widgets/api_search_bar.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; + +class ApiSearchBar extends StatefulWidget { + final String hintText; + final ValueChanged? onChanged; + final VoidCallback? onClear; + + const ApiSearchBar({ + super.key, + this.hintText = 'Search...', + this.onChanged, + this.onClear, + }); + + @override + ApiSearchBarState createState() => ApiSearchBarState(); +} + +class ApiSearchBarState extends State { + final TextEditingController _controller = TextEditingController(); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + height: 36, // Smaller height + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: const BorderRadius.horizontal( + left: Radius.circular(18), + right: Radius.circular(18), + ), + ), + child: TextField( + controller: _controller, + onChanged: widget.onChanged, + style: const TextStyle(fontSize: 14), + decoration: InputDecoration( + hintText: widget.hintText, + hintStyle: const TextStyle(fontSize: 14), + prefixIcon: const Icon(Icons.search, size: 18), + suffixIcon: _controller.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear, size: 16), + onPressed: () { + _controller.clear(); + widget.onChanged?.call(''); + widget.onClear?.call(); + }, + ) + : null, + border: OutlineInputBorder( + borderRadius: const BorderRadius.horizontal( + left: Radius.circular(18), + right: Radius.circular(18), + ), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 8.0, + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/explorer/explorer_body.dart b/lib/screens/explorer/explorer_body.dart new file mode 100644 index 000000000..17e11c169 --- /dev/null +++ b/lib/screens/explorer/explorer_body.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import '../explorer/common_widgets/api_search_bar.dart'; + +class ExplorerBody extends StatelessWidget { + const ExplorerBody({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + color: Theme.of(context).colorScheme.background, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Center( + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.6, + child: ApiSearchBar( + hintText: 'Search Explorer', + onChanged: (value) { + // TODO: Handle search input + }, + onClear: () { + // TODO:Handle clear action + }, + ), + ), + ), + ), + // Content area + const Expanded( + child: Center( + child: Text( + 'Explorer Body Content', + style: TextStyle(fontSize: 24), + ), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/explorer/explorer_header.dart b/lib/screens/explorer/explorer_header.dart new file mode 100644 index 000000000..5800e2270 --- /dev/null +++ b/lib/screens/explorer/explorer_header.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/screens/explorer/common_widgets/import.dart'; + +class ExplorerHeader extends StatelessWidget { + const ExplorerHeader({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + color: Theme.of(context).colorScheme.background, + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'API EXPLORER', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const ImportButton(), + ], + ), + ); + } +} diff --git a/lib/screens/explorer/explorer_page.dart b/lib/screens/explorer/explorer_page.dart new file mode 100644 index 000000000..e5f46616a --- /dev/null +++ b/lib/screens/explorer/explorer_page.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'explorer_header.dart'; +import 'explorer_body.dart'; + +class ExplorerPage extends StatelessWidget { + const ExplorerPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + const SizedBox( + height: 60, + child: ExplorerHeader(), + ), + const Expanded( + child: ExplorerBody(), + ), + ], + ), + ); + } +} \ No newline at end of file From 4c9c9a03869b74fe26cc99646ac06ceaad48c14d Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Sat, 19 Apr 2025 14:32:46 +0530 Subject: [PATCH 02/25] feat: add API template model, card UI, and template loading service -Introduced ApiTemplate, Info, and Request model classes to represent structured API template data. -Added TemplateCard, CardTitle, and CardDescription UI components for rendering API templates. -Implemented TemplatesService to dynamically load .json templates from asset directory. -Updated ExplorerBody to fetch and display API templates using a responsive grid layout. Signed-off-by: Balasubramaniam12007 --- lib/models/explorer_model.dart | 96 +++++++++++++++++++ lib/models/models.dart | 1 + .../api_templates/mock/blog_post.json | 82 ++++++++++++++++ .../api_templates/mock/event_api.json | 82 ++++++++++++++++ .../api_templates/mock/order_api.json | 59 ++++++++++++ .../explorer/api_templates/mock/product.json | 84 ++++++++++++++++ .../mock/user_management_api.json | 84 ++++++++++++++++ .../common_widgets/card_description.dart | 39 ++++++++ .../explorer/common_widgets/card_title.dart | 48 ++++++++++ .../common_widgets/template_card.dart | 61 ++++++++++++ lib/screens/explorer/explorer_body.dart | 70 +++++++++++--- lib/services/services.dart | 1 + lib/services/templates_service.dart | 47 +++++++++ pubspec.yaml | 2 +- 14 files changed, 743 insertions(+), 13 deletions(-) create mode 100644 lib/models/explorer_model.dart create mode 100644 lib/screens/explorer/api_templates/mock/blog_post.json create mode 100644 lib/screens/explorer/api_templates/mock/event_api.json create mode 100644 lib/screens/explorer/api_templates/mock/order_api.json create mode 100644 lib/screens/explorer/api_templates/mock/product.json create mode 100644 lib/screens/explorer/api_templates/mock/user_management_api.json create mode 100644 lib/screens/explorer/common_widgets/card_description.dart create mode 100644 lib/screens/explorer/common_widgets/card_title.dart create mode 100644 lib/screens/explorer/common_widgets/template_card.dart create mode 100644 lib/services/templates_service.dart diff --git a/lib/models/explorer_model.dart b/lib/models/explorer_model.dart new file mode 100644 index 000000000..f7171c23c --- /dev/null +++ b/lib/models/explorer_model.dart @@ -0,0 +1,96 @@ +class ApiTemplate { + final Info info; + final List requests; + + ApiTemplate({required this.info, required this.requests}); + + /// Parses JSON data into an ApiTemplate object. + /// Future extensions: Add support for additional top-level fields (e.g., version, author). + factory ApiTemplate.fromJson(Map json) { + return ApiTemplate( + info: Info.fromJson(json['info'] ?? {}), + requests: (json['requests'] as List?) + ?.map((request) => Request.fromJson(request)) + .toList() ?? + [], + ); + } + + /// Converts the ApiTemplate back to JSON (useful for saving or debugging). + /// Future extensions: Add serialization for new fields. + Map toJson() { + return { + 'info': info.toJson(), + 'requests': requests.map((request) => request.toJson()).toList(), + }; + } +} + +/// Represents metadata (e.g., title, description, tags). +class Info { + final String title; + final String description; + final List tags; + + Info({ + required this.title, + required this.description, + required this.tags, + }); + + /// Parses JSON data into an Info object. + /// Future extensions: Add fields like category, version, or lastUpdated. + factory Info.fromJson(Map json) { + return Info( + title: json['title'] ?? 'Untitled', + description: json['description'] ?? 'No description', + tags: List.from(json['tags'] ?? []), + ); + } + + /// Converts the Info object back to JSON. + Map toJson() { + return { + 'title': title, + 'description': description, + 'tags': tags, + }; + } +} + +/// Represents a single API request within a template. +class Request { + final String id; + final String apiType; + final String name; + final String description; + // Add more fields as needed (e.g., httpRequestModel, responseStatus). + + Request({ + required this.id, + required this.apiType, + required this.name, + required this.description, + }); + + /// Parses JSON data into a Request object. + /// Future extensions: Add parsing for httpRequestModel or other nested structures. + factory Request.fromJson(Map json) { + return Request( + id: json['id'] ?? '', + apiType: json['apiType'] ?? 'unknown', + name: json['name'] ?? 'Unnamed', + description: json['description'] ?? 'No description', + ); + } + + /// Converts the Request object back to JSON. + Map toJson() { + return { + 'id': id, + 'apiType': apiType, + 'name': name, + 'description': description, + }; + } +} \ No newline at end of file diff --git a/lib/models/models.dart b/lib/models/models.dart index 5d17479a0..d6be820c1 100644 --- a/lib/models/models.dart +++ b/lib/models/models.dart @@ -2,3 +2,4 @@ export 'history_meta_model.dart'; export 'history_request_model.dart'; export 'request_model.dart'; export 'settings_model.dart'; +export 'explorer_model.dart'; diff --git a/lib/screens/explorer/api_templates/mock/blog_post.json b/lib/screens/explorer/api_templates/mock/blog_post.json new file mode 100644 index 000000000..f4425687c --- /dev/null +++ b/lib/screens/explorer/api_templates/mock/blog_post.json @@ -0,0 +1,82 @@ +{ + "info": { + "title": "Blog Post API", + "description": "API for creating, retrieving, and deleting blog posts", + "tags": ["blog", "posts", "content"] + }, + "requests": [ + { + "id": "post_create", + "apiType": "rest", + "name": "Create a blog post", + "description": "Publishes a new blog post", + "httpRequestModel": { + "method": "post", + "url": "https://api.example.com/v1/posts", + "headers": [ + {"name": "Content-Type", "value": "application/json"} + ], + "params": [], + "isHeaderEnabledList": [true], + "isParamEnabledList": [], + "bodyContentType": "json", + "body": "{\"title\": \"My First Post\", \"content\": \"This is a blog post\", \"author\": \"Jane Doe\"}", + "query": null, + "formData": null + }, + "responseStatus": null, + "message": null, + "httpResponseModel": null, + "isWorking": false, + "sendingTime": null + }, + { + "id": "post_get_all", + "apiType": "rest", + "name": "Get all blog posts", + "description": "Retrieves a list of all blog posts", + "httpRequestModel": { + "method": "get", + "url": "https://api.example.com/v1/posts", + "headers": [], + "params": [], + "isHeaderEnabledList": [], + "isParamEnabledList": [], + "bodyContentType": "json", + "body": null, + "query": null, + "formData": null + }, + "responseStatus": null, + "message": null, + "httpResponseModel": null, + "isWorking": false, + "sendingTime": null + }, + { + "id": "post_delete", + "apiType": "rest", + "name": "Delete a blog post", + "description": "Removes a blog post by ID", + "httpRequestModel": { + "method": "delete", + "url": "https://api.example.com/v1/posts/1", + "headers": [], + "params": [ + {"name": "id", "value": "1"} + ], + "isHeaderEnabledList": [], + "isParamEnabledList": [true], + "bodyContentType": "json", + "body": null, + "query": null, + "formData": null + }, + "responseStatus": null, + "message": null, + "httpResponseModel": null, + "isWorking": false, + "sendingTime": null + } + ] + } \ No newline at end of file diff --git a/lib/screens/explorer/api_templates/mock/event_api.json b/lib/screens/explorer/api_templates/mock/event_api.json new file mode 100644 index 000000000..e61c1ddc7 --- /dev/null +++ b/lib/screens/explorer/api_templates/mock/event_api.json @@ -0,0 +1,82 @@ +{ + "info": { + "title": "Event Booking API", + "description": "API for booking and managing event tickets", + "tags": ["events", "bookings", "tickets"] + }, + "requests": [ + { + "id": "event_book", + "apiType": "rest", + "name": "Book an event ticket", + "description": "Books a ticket for a specific event", + "httpRequestModel": { + "method": "post", + "url": "https://api.example.com/v1/bookings", + "headers": [ + {"name": "Content-Type", "value": "application/json"} + ], + "params": [], + "isHeaderEnabledList": [true], + "isParamEnabledList": [], + "bodyContentType": "json", + "body": "{\"eventId\": \"evt456\", \"userId\": \"user789\", \"quantity\": 2}", + "query": null, + "formData": null + }, + "responseStatus": null, + "message": null, + "httpResponseModel": null, + "isWorking": false, + "sendingTime": null + }, + { + "id": "event_get_bookings", + "apiType": "rest", + "name": "Get all bookings", + "description": "Retrieves a list of all event bookings", + "httpRequestModel": { + "method": "get", + "url": "https://api.example.com/v1/bookings", + "headers": [], + "params": [], + "isHeaderEnabledList": [], + "isParamEnabledList": [], + "bodyContentType": "json", + "body": null, + "query": null, + "formData": null + }, + "responseStatus": null, + "message": null, + "httpResponseModel": null, + "isWorking": false, + "sendingTime": null + }, + { + "id": "event_cancel_booking", + "apiType": "rest", + "name": "Cancel a booking", + "description": "Cancels an event booking by ID", + "httpRequestModel": { + "method": "delete", + "url": "https://api.example.com/v1/bookings/123", + "headers": [], + "params": [ + {"name": "id", "value": "123"} + ], + "isHeaderEnabledList": [], + "isParamEnabledList": [true], + "bodyContentType": "json", + "body": null, + "query": null, + "formData": null + }, + "responseStatus": null, + "message": null, + "httpResponseModel": null, + "isWorking": false, + "sendingTime": null + } + ] + } \ No newline at end of file diff --git a/lib/screens/explorer/api_templates/mock/order_api.json b/lib/screens/explorer/api_templates/mock/order_api.json new file mode 100644 index 000000000..c43c06482 --- /dev/null +++ b/lib/screens/explorer/api_templates/mock/order_api.json @@ -0,0 +1,59 @@ +{ + "info": { + "title": "Order Processing API", + "description": "API for processing customer orders and checking order status", + "tags": ["orders", "e-commerce", "processing"] + }, + "requests": [ + { + "id": "order_create", + "apiType": "rest", + "name": "Create an order", + "description": "Places a new customer order", + "httpRequestModel": { + "method": "post", + "url": "https://api.example.com/v1/orders", + "headers": [ + {"name": "Content-Type", "value": "application/json"} + ], + "params": [], + "isHeaderEnabledList": [true], + "isParamEnabledList": [], + "bodyContentType": "json", + "body": "{\"customerId\": \"cust123\", \"items\": [{\"productId\": \"101\", \"quantity\": 2}]}", + "query": null, + "formData": null + }, + "responseStatus": null, + "message": null, + "httpResponseModel": null, + "isWorking": false, + "sendingTime": null + }, + { + "id": "order_status", + "apiType": "rest", + "name": "Get order status", + "description": "Fetches the status of a specific order", + "httpRequestModel": { + "method": "get", + "url": "https://api.example.com/v1/orders/1001", + "headers": [], + "params": [ + {"name": "id", "value": "1001"} + ], + "isHeaderEnabledList": [], + "isParamEnabledList": [true], + "bodyContentType": "json", + "body": null, + "query": null, + "formData": null + }, + "responseStatus": null, + "message": null, + "httpResponseModel": null, + "isWorking": false, + "sendingTime": null + } + ] + } \ No newline at end of file diff --git a/lib/screens/explorer/api_templates/mock/product.json b/lib/screens/explorer/api_templates/mock/product.json new file mode 100644 index 000000000..4b6a2b169 --- /dev/null +++ b/lib/screens/explorer/api_templates/mock/product.json @@ -0,0 +1,84 @@ +{ + "info": { + "title": "Product Catalog API", + "description": "API for managing a product catalog, including listing and adding products", + "tags": ["products", "catalog", "e-commerce"] + }, + "requests": [ + { + "id": "product_list", + "apiType": "rest", + "name": "List all products", + "description": "Retrieves a list of all products in the catalog", + "httpRequestModel": { + "method": "get", + "url": "https://api.example.com/v1/products", + "headers": [], + "params": [ + {"name": "category", "value": "electronics"} + ], + "isHeaderEnabledList": [], + "isParamEnabledList": [true], + "bodyContentType": "json", + "body": null, + "query": null, + "formData": null + }, + "responseStatus": null, + "message": null, + "httpResponseModel": null, + "isWorking": false, + "sendingTime": null + }, + { + "id": "product_get", + "apiType": "rest", + "name": "Get product details", + "description": "Fetches details of a specific product by ID", + "httpRequestModel": { + "method": "get", + "url": "https://api.example.com/v1/products/101", + "headers": [], + "params": [ + {"name": "id", "value": "101"} + ], + "isHeaderEnabledList": [], + "isParamEnabledList": [true], + "bodyContentType": "json", + "body": null, + "query": null, + "formData": null + }, + "responseStatus": null, + "message": null, + "httpResponseModel": null, + "isWorking": false, + "sendingTime": null + }, + { + "id": "product_add", + "apiType": "rest", + "name": "Add a new product", + "description": "Adds a new product to the catalog", + "httpRequestModel": { + "method": "post", + "url": "https://api.example.com/v1/products", + "headers": [ + {"name": "Content-Type", "value": "application/json"} + ], + "params": [], + "isHeaderEnabledList": [true], + "isParamEnabledList": [], + "bodyContentType": "json", + "body": "{\"name\": \"Smartphone\", \"price\": 599.99, \"category\": \"electronics\"}", + "query": null, + "formData": null + }, + "responseStatus": null, + "message": null, + "httpResponseModel": null, + "isWorking": false, + "sendingTime": null + } + ] + } \ No newline at end of file diff --git a/lib/screens/explorer/api_templates/mock/user_management_api.json b/lib/screens/explorer/api_templates/mock/user_management_api.json new file mode 100644 index 000000000..b4a7265b7 --- /dev/null +++ b/lib/screens/explorer/api_templates/mock/user_management_api.json @@ -0,0 +1,84 @@ +{ + "info": { + "title": "User Management API", + "description": "API for managing user accounts, including registration, retrieval, and updates", + "tags": ["users", "management", "authentication"] + }, + "requests": [ + { + "id": "user_register", + "apiType": "rest", + "name": "Register a new user", + "description": "Creates a new user account", + "httpRequestModel": { + "method": "post", + "url": "https://api.example.com/v1/users", + "headers": [ + {"name": "Content-Type", "value": "application/json"} + ], + "params": [], + "isHeaderEnabledList": [true], + "isParamEnabledList": [], + "bodyContentType": "json", + "body": "{\"username\": \"johndoe\", \"email\": \"john@example.com\", \"password\": \"secure123\"}", + "query": null, + "formData": null + }, + "responseStatus": null, + "message": null, + "httpResponseModel": null, + "isWorking": false, + "sendingTime": null + }, + { + "id": "user_get_all", + "apiType": "rest", + "name": "Get all users", + "description": "Fetches a list of all users", + "httpRequestModel": { + "method": "get", + "url": "https://api.example.com/v1/users", + "headers": [], + "params": [], + "isHeaderEnabledList": [], + "isParamEnabledList": [], + "bodyContentType": "json", + "body": null, + "query": null, + "formData": null + }, + "responseStatus": null, + "message": null, + "httpResponseModel": null, + "isWorking": false, + "sendingTime": null + }, + { + "id": "user_update", + "apiType": "rest", + "name": "Update user details", + "description": "Updates details of a specific user by ID", + "httpRequestModel": { + "method": "put", + "url": "https://api.example.com/v1/users/1", + "headers": [ + {"name": "Content-Type", "value": "application/json"} + ], + "params": [ + {"name": "id", "value": "1"} + ], + "isHeaderEnabledList": [true], + "isParamEnabledList": [true], + "bodyContentType": "json", + "body": "{\"email\": \"john.doe@example.com\"}", + "query": null, + "formData": null + }, + "responseStatus": null, + "message": null, + "httpResponseModel": null, + "isWorking": false, + "sendingTime": null + } + ] + } \ No newline at end of file diff --git a/lib/screens/explorer/common_widgets/card_description.dart b/lib/screens/explorer/common_widgets/card_description.dart new file mode 100644 index 000000000..ae38ac8ba --- /dev/null +++ b/lib/screens/explorer/common_widgets/card_description.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + +class CardDescription extends StatelessWidget { + final String description; + final int maxLines; + + const CardDescription({ + Key? key, + required this.description, + this.maxLines = 2, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return SizedBox( + height: 80, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceVariant.withOpacity(0.3), + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Text( + description.isEmpty ? 'No description' : description, + textAlign: TextAlign.left, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface.withOpacity(0.7), + ), + maxLines: maxLines, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/explorer/common_widgets/card_title.dart b/lib/screens/explorer/common_widgets/card_title.dart new file mode 100644 index 000000000..7188daea9 --- /dev/null +++ b/lib/screens/explorer/common_widgets/card_title.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +class CardTitle extends StatelessWidget { + final String title; + final IconData icon; + final Color? iconColor; + + const CardTitle({ + Key? key, + required this.title, + required this.icon, + this.iconColor, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: (iconColor ?? theme.colorScheme.primary).withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + icon, + size: 18, + color: iconColor ?? theme.colorScheme.primary, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + title, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/explorer/common_widgets/template_card.dart b/lib/screens/explorer/common_widgets/template_card.dart new file mode 100644 index 000000000..27b8fdf59 --- /dev/null +++ b/lib/screens/explorer/common_widgets/template_card.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'card_title.dart'; +import 'card_description.dart'; + + +class TemplateCard extends StatelessWidget { + final String id; + final String name; + final String description; + final IconData? icon; + final VoidCallback? onTap; + + const TemplateCard({ + Key? key, + required this.id, + required this.name, + required this.description, + this.icon, + this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Card( + margin: const EdgeInsets.all(8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide( + color: colorScheme.outline.withOpacity(0.2), + width: 1, + ), + ), + color: colorScheme.surface, + elevation: 0.5, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(16), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CardTitle( + title: name, + icon: icon ?? Icons.api, //currently no icons in the templates so icon always Icons.api + iconColor: colorScheme.primary, + ), + const SizedBox(height: 8), + CardDescription( + description: description.isEmpty ? 'No description' : description, + maxLines: 2, + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/explorer/explorer_body.dart b/lib/screens/explorer/explorer_body.dart index 17e11c169..d1e03d8c5 100644 --- a/lib/screens/explorer/explorer_body.dart +++ b/lib/screens/explorer/explorer_body.dart @@ -1,9 +1,25 @@ import 'package:flutter/material.dart'; -import '../explorer/common_widgets/api_search_bar.dart'; +import 'package:apidash/services/services.dart'; +import 'common_widgets/template_card.dart'; +import 'common_widgets/api_search_bar.dart'; +import 'package:apidash/models/models.dart'; -class ExplorerBody extends StatelessWidget { +class ExplorerBody extends StatefulWidget { const ExplorerBody({super.key}); - + + @override + _ExplorerBodyState createState() => _ExplorerBodyState(); +} + +class _ExplorerBodyState extends State { + late Future> _templatesFuture; + + @override + void initState() { + super.initState(); + _templatesFuture = TemplatesService.loadTemplates(); + } + @override Widget build(BuildContext context) { return Container( @@ -14,11 +30,12 @@ class ExplorerBody extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 16.0), child: Center( child: SizedBox( - width: MediaQuery.of(context).size.width * 0.6, + width: MediaQuery.of(context).size.width * 0.6, child: ApiSearchBar( hintText: 'Search Explorer', onChanged: (value) { - // TODO: Handle search input + // TODO: Implement search filtering + // Example: Filter templates by title or tags }, onClear: () { // TODO:Handle clear action @@ -27,13 +44,42 @@ class ExplorerBody extends StatelessWidget { ), ), ), - // Content area - const Expanded( - child: Center( - child: Text( - 'Explorer Body Content', - style: TextStyle(fontSize: 24), - ), + Expanded( + child: FutureBuilder>( + future: _templatesFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } else if (!snapshot.hasData || snapshot.data!.isEmpty) { + return const Center(child: Text('No templates found')); + } + + final templates = snapshot.data!; + return GridView.builder( + padding: const EdgeInsets.all(12), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 300, // Maximum width of each card + childAspectRatio: 1.3, // Height-to-width ratio + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + itemCount: templates.length, + itemBuilder: (context, index) { + final template = templates[index]; + return TemplateCard( + id: template.info.title, + name: template.info.title, + description: template.info.description, + icon: Icons.api, + onTap: () { + // TODO: Handle card tap (navigate to details screen) + }, + ); + }, + ); + }, ), ), ], diff --git a/lib/services/services.dart b/lib/services/services.dart index 7fa8128d6..04f069005 100644 --- a/lib/services/services.dart +++ b/lib/services/services.dart @@ -2,3 +2,4 @@ export 'hive_services.dart'; export 'history_service.dart'; export 'window_services.dart'; export 'shared_preferences_services.dart'; +export 'templates_service.dart'; diff --git a/lib/services/templates_service.dart b/lib/services/templates_service.dart new file mode 100644 index 000000000..2dfa87fc7 --- /dev/null +++ b/lib/services/templates_service.dart @@ -0,0 +1,47 @@ +import 'dart:convert'; +import 'package:flutter/services.dart' show DefaultAssetBundle, rootBundle; +import 'package:apidash/models/models.dart'; + +class TemplatesService { + + static Future> loadTemplates() async { + + const String templatesDir = 'lib/screens/explorer/api_templates/mock'; // Directory containing JSON files + + try { + final manifestContent = await rootBundle.loadString('AssetManifest.json'); + final Map manifestMap = jsonDecode(manifestContent); + + // Filter for JSON files in the templates directory + final jsonFiles = manifestMap.keys + .where((key) => key.startsWith(templatesDir) && key.endsWith('.json')) + .toList(); + + List templates = []; + for (String filePath in jsonFiles) { + try { + final String jsonString = await rootBundle.loadString(filePath); + final Map jsonData = jsonDecode(jsonString); + templates.add(ApiTemplate.fromJson(jsonData)); + } catch (e) { + print('Error loading $filePath: $e'); + // Future extensions: Log errors to a monitoring service. + } + } + + return templates; + } catch (e) { + print('Error loading templates: $e'); + return []; + } + } + + static Future> fetchTemplatesFromApi() async { + // Example implementation: + // final response = await http.get(Uri.parse('https://api.example.com/templates')); + // final List jsonList = jsonDecode(response.body); + // return jsonList.map((json) => ApiTemplate.fromJson(json)).toList(); + return []; + } + +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index d4c2ae41a..e73edd429 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -93,4 +93,4 @@ dev_dependencies: flutter: uses-material-design: true assets: - - assets/ + - lib/screens/explorer/api_templates/mock/ From 2cf06df53aba70498528c31e8b397ec3639a0324 Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Sun, 20 Apr 2025 11:41:50 +0530 Subject: [PATCH 03/25] feat: Add DescriptionPage with navigable UI from ExplorerPage - Refactored ExplorerPage to a StatefulWidget to support navigation - Introduced _showDescription state to conditionally switch between explorer and description views - Implemented DescriptionPage as a new UI component for viewing API details - Added DescriptionHeader with back navigation functionality - Created DescriptionBody which includes MethodPane and DescriptionPane - Modified ExplorerBody to accept onCardTap callback and pass it to TemplateCard tap action Signed-off-by: Balasubramaniam12007 --- .../description/description_body.dart | 25 ++++++++++ .../description/description_header.dart | 31 ++++++++++++ .../description/description_page.dart | 27 +++++++++++ .../description/description_pane.dart | 18 +++++++ .../explorer/description/method_pane.dart | 27 +++++++++++ lib/screens/explorer/explorer_body.dart | 15 +++--- lib/screens/explorer/explorer_page.dart | 48 ++++++++++++++----- 7 files changed, 173 insertions(+), 18 deletions(-) create mode 100644 lib/screens/explorer/description/description_body.dart create mode 100644 lib/screens/explorer/description/description_header.dart create mode 100644 lib/screens/explorer/description/description_page.dart create mode 100644 lib/screens/explorer/description/description_pane.dart create mode 100644 lib/screens/explorer/description/method_pane.dart diff --git a/lib/screens/explorer/description/description_body.dart b/lib/screens/explorer/description/description_body.dart new file mode 100644 index 000000000..27b0b9c60 --- /dev/null +++ b/lib/screens/explorer/description/description_body.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'method_pane.dart'; +import 'description_pane.dart'; + +class DescriptionBody extends StatelessWidget { + const DescriptionBody({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + color: Theme.of(context).colorScheme.background, + child: Row( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width * 0.3, + child: const MethodPane(), + ), + const Expanded( + child: DescriptionPane(), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/explorer/description/description_header.dart b/lib/screens/explorer/description/description_header.dart new file mode 100644 index 000000000..f5d7d4cbe --- /dev/null +++ b/lib/screens/explorer/description/description_header.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +class DescriptionHeader extends StatelessWidget { + final VoidCallback onBack; + + const DescriptionHeader({ + super.key, + required this.onBack, + }); + + @override + Widget build(BuildContext context) { + return Container( + color: Theme.of(context).colorScheme.background, + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: onBack, + ), + const SizedBox(width: 8), + const Text( + 'Header', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/explorer/description/description_page.dart b/lib/screens/explorer/description/description_page.dart new file mode 100644 index 000000000..fc16c6b2d --- /dev/null +++ b/lib/screens/explorer/description/description_page.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'description_header.dart'; +import 'description_body.dart'; + +class DescriptionPage extends StatelessWidget { + final VoidCallback onBack; + + const DescriptionPage({ + super.key, + required this.onBack, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SizedBox( + height: 60, + child: DescriptionHeader(onBack: onBack), + ), + const Expanded( + child: DescriptionBody(), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/explorer/description/description_pane.dart b/lib/screens/explorer/description/description_pane.dart new file mode 100644 index 000000000..3b94283ee --- /dev/null +++ b/lib/screens/explorer/description/description_pane.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class DescriptionPane extends StatelessWidget { + const DescriptionPane({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.white, + child: const Center( + child: Text( + 'Description Pane', + style: TextStyle(fontSize: 18), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/explorer/description/method_pane.dart b/lib/screens/explorer/description/method_pane.dart new file mode 100644 index 000000000..8ba3f6bb0 --- /dev/null +++ b/lib/screens/explorer/description/method_pane.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class MethodPane extends StatelessWidget { + const MethodPane({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + color: Theme.of(context).colorScheme.surface, + padding: const EdgeInsets.all(12.0), + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Method Pane', // Placeholder + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + SizedBox(height: 8), + Text( + 'This pane will display API method details (e.g., GET, POST).', // Placeholder + style: TextStyle(fontSize: 14), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/explorer/explorer_body.dart b/lib/screens/explorer/explorer_body.dart index d1e03d8c5..50501ce9d 100644 --- a/lib/screens/explorer/explorer_body.dart +++ b/lib/screens/explorer/explorer_body.dart @@ -5,7 +5,12 @@ import 'common_widgets/api_search_bar.dart'; import 'package:apidash/models/models.dart'; class ExplorerBody extends StatefulWidget { - const ExplorerBody({super.key}); + final VoidCallback? onCardTap; + + const ExplorerBody({ + super.key, + this.onCardTap, + }); @override _ExplorerBodyState createState() => _ExplorerBodyState(); @@ -60,8 +65,8 @@ class _ExplorerBodyState extends State { return GridView.builder( padding: const EdgeInsets.all(12), gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 300, // Maximum width of each card - childAspectRatio: 1.3, // Height-to-width ratio + maxCrossAxisExtent: 300, + childAspectRatio: 1.3, crossAxisSpacing: 12, mainAxisSpacing: 12, ), @@ -73,9 +78,7 @@ class _ExplorerBodyState extends State { name: template.info.title, description: template.info.description, icon: Icons.api, - onTap: () { - // TODO: Handle card tap (navigate to details screen) - }, + onTap: widget.onCardTap, ); }, ); diff --git a/lib/screens/explorer/explorer_page.dart b/lib/screens/explorer/explorer_page.dart index e5f46616a..ead558be8 100644 --- a/lib/screens/explorer/explorer_page.dart +++ b/lib/screens/explorer/explorer_page.dart @@ -1,24 +1,48 @@ import 'package:flutter/material.dart'; import 'explorer_header.dart'; import 'explorer_body.dart'; +import 'description/description_page.dart'; -class ExplorerPage extends StatelessWidget { +class ExplorerPage extends StatefulWidget { const ExplorerPage({super.key}); + @override + State createState() => _ExplorerPageState(); +} + +class _ExplorerPageState extends State { + bool _showDescription = false; + + void _navigateToDescription() { + setState(() { + _showDescription = true; + }); + } + + void _navigateBackToExplorer() { + setState(() { + _showDescription = false; + }); + } + @override Widget build(BuildContext context) { return Scaffold( - body: Column( - children: [ - const SizedBox( - height: 60, - child: ExplorerHeader(), - ), - const Expanded( - child: ExplorerBody(), - ), - ], - ), + body: _showDescription + ? DescriptionPage(onBack: _navigateBackToExplorer) + : Column( + children: [ + const SizedBox( + height: 60, + child: ExplorerHeader(), + ), + Expanded( + child: ExplorerBody( + onCardTap: _navigateToDescription, + ), + ), + ], + ), ); } } \ No newline at end of file From 005ff587437d895cc518d4aea7b59087dd1b90eb Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Mon, 21 Apr 2025 18:34:14 +0530 Subject: [PATCH 04/25] (Feat)(Fix): Reconstruct explorer_model to use RequestModel, pass ApiTemplate to TemplateCard, enhance DescriptionHeader with info.description and tags, fix overflow error - Updated explorer_model.dart to replace custom Request with RequestModel from apidash_core, ensuring mock data compatibility. - Modified ExplorerBody and TemplateCard to pass ApiTemplate, preserving card UI (title, description, icon). - Enhanced DescriptionPage and DescriptionHeader to display info.title, description, and tags with smaller font, maintaining theme consistency. - Fixed 50-pixel overflow in DescriptionHeader by removing fixed height in DescriptionPage. Signed-off-by: Balasubramaniam12007 --- lib/models/explorer_model.dart | 49 +++---------------- .../common_widgets/template_card.dart | 18 +++---- .../description/description_header.dart | 33 +++++++++++-- .../description/description_page.dart | 9 ++-- lib/screens/explorer/explorer_body.dart | 11 ++--- lib/screens/explorer/explorer_page.dart | 15 ++++-- 6 files changed, 62 insertions(+), 73 deletions(-) diff --git a/lib/models/explorer_model.dart b/lib/models/explorer_model.dart index f7171c23c..c3ae6cceb 100644 --- a/lib/models/explorer_model.dart +++ b/lib/models/explorer_model.dart @@ -1,23 +1,23 @@ +import 'models.dart'; + class ApiTemplate { final Info info; - final List requests; + final List requests; ApiTemplate({required this.info, required this.requests}); /// Parses JSON data into an ApiTemplate object. - /// Future extensions: Add support for additional top-level fields (e.g., version, author). factory ApiTemplate.fromJson(Map json) { return ApiTemplate( info: Info.fromJson(json['info'] ?? {}), - requests: (json['requests'] as List?) - ?.map((request) => Request.fromJson(request)) + requests: (json['requests'] as List?) + ?.map((request) => RequestModel.fromJson(request)) .toList() ?? [], ); } - /// Converts the ApiTemplate back to JSON (useful for saving or debugging). - /// Future extensions: Add serialization for new fields. + /// Converts the ApiTemplate back to JSON. Map toJson() { return { 'info': info.toJson(), @@ -57,40 +57,3 @@ class Info { }; } } - -/// Represents a single API request within a template. -class Request { - final String id; - final String apiType; - final String name; - final String description; - // Add more fields as needed (e.g., httpRequestModel, responseStatus). - - Request({ - required this.id, - required this.apiType, - required this.name, - required this.description, - }); - - /// Parses JSON data into a Request object. - /// Future extensions: Add parsing for httpRequestModel or other nested structures. - factory Request.fromJson(Map json) { - return Request( - id: json['id'] ?? '', - apiType: json['apiType'] ?? 'unknown', - name: json['name'] ?? 'Unnamed', - description: json['description'] ?? 'No description', - ); - } - - /// Converts the Request object back to JSON. - Map toJson() { - return { - 'id': id, - 'apiType': apiType, - 'name': name, - 'description': description, - }; - } -} \ No newline at end of file diff --git a/lib/screens/explorer/common_widgets/template_card.dart b/lib/screens/explorer/common_widgets/template_card.dart index 27b8fdf59..830508dfc 100644 --- a/lib/screens/explorer/common_widgets/template_card.dart +++ b/lib/screens/explorer/common_widgets/template_card.dart @@ -1,21 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:apidash/models/models.dart'; import 'card_title.dart'; import 'card_description.dart'; - class TemplateCard extends StatelessWidget { - final String id; - final String name; - final String description; - final IconData? icon; + final ApiTemplate template; final VoidCallback? onTap; const TemplateCard({ Key? key, - required this.id, - required this.name, - required this.description, - this.icon, + required this.template, this.onTap, }) : super(key: key); @@ -43,13 +37,13 @@ class TemplateCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ CardTitle( - title: name, - icon: icon ?? Icons.api, //currently no icons in the templates so icon always Icons.api + title: template.info.title, + icon: Icons.api, //currently no icons in the templates so icon always Icons.api iconColor: colorScheme.primary, ), const SizedBox(height: 8), CardDescription( - description: description.isEmpty ? 'No description' : description, + description: template.info.description.isEmpty ? 'No description' : template.info.description, maxLines: 2, ), ], diff --git a/lib/screens/explorer/description/description_header.dart b/lib/screens/explorer/description/description_header.dart index f5d7d4cbe..6bf98b400 100644 --- a/lib/screens/explorer/description/description_header.dart +++ b/lib/screens/explorer/description/description_header.dart @@ -1,17 +1,21 @@ import 'package:flutter/material.dart'; +import 'package:apidash/models/models.dart'; class DescriptionHeader extends StatelessWidget { + final Info info; final VoidCallback onBack; const DescriptionHeader({ super.key, + required this.info, required this.onBack, }); @override Widget build(BuildContext context) { + final theme = Theme.of(context); return Container( - color: Theme.of(context).colorScheme.background, + color: theme.colorScheme.background, padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Row( children: [ @@ -20,9 +24,30 @@ class DescriptionHeader extends StatelessWidget { onPressed: onBack, ), const SizedBox(width: 8), - const Text( - 'Header', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + info.title, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (info.description.isNotEmpty) + Text( + info.description, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurface.withOpacity(0.7), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), ), ], ), diff --git a/lib/screens/explorer/description/description_page.dart b/lib/screens/explorer/description/description_page.dart index fc16c6b2d..3cbb4ee3f 100644 --- a/lib/screens/explorer/description/description_page.dart +++ b/lib/screens/explorer/description/description_page.dart @@ -1,12 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:apidash/models/models.dart'; import 'description_header.dart'; import 'description_body.dart'; class DescriptionPage extends StatelessWidget { + final ApiTemplate template; final VoidCallback onBack; const DescriptionPage({ super.key, + required this.template, required this.onBack, }); @@ -14,9 +17,9 @@ class DescriptionPage extends StatelessWidget { Widget build(BuildContext context) { return Column( children: [ - SizedBox( - height: 60, - child: DescriptionHeader(onBack: onBack), + DescriptionHeader( + info: template.info, + onBack: onBack, ), const Expanded( child: DescriptionBody(), diff --git a/lib/screens/explorer/explorer_body.dart b/lib/screens/explorer/explorer_body.dart index 50501ce9d..f5707f09e 100644 --- a/lib/screens/explorer/explorer_body.dart +++ b/lib/screens/explorer/explorer_body.dart @@ -5,8 +5,8 @@ import 'common_widgets/api_search_bar.dart'; import 'package:apidash/models/models.dart'; class ExplorerBody extends StatefulWidget { - final VoidCallback? onCardTap; - + final Function(ApiTemplate)? onCardTap; + const ExplorerBody({ super.key, this.onCardTap, @@ -74,11 +74,8 @@ class _ExplorerBodyState extends State { itemBuilder: (context, index) { final template = templates[index]; return TemplateCard( - id: template.info.title, - name: template.info.title, - description: template.info.description, - icon: Icons.api, - onTap: widget.onCardTap, + template: template, + onTap: () => widget.onCardTap?.call(template), ); }, ); diff --git a/lib/screens/explorer/explorer_page.dart b/lib/screens/explorer/explorer_page.dart index ead558be8..6778d416c 100644 --- a/lib/screens/explorer/explorer_page.dart +++ b/lib/screens/explorer/explorer_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:apidash/models/models.dart'; import 'explorer_header.dart'; import 'explorer_body.dart'; import 'description/description_page.dart'; @@ -12,28 +13,34 @@ class ExplorerPage extends StatefulWidget { class _ExplorerPageState extends State { bool _showDescription = false; + ApiTemplate? _selectedTemplate; - void _navigateToDescription() { + void _navigateToDescription(ApiTemplate template) { setState(() { _showDescription = true; + _selectedTemplate = template; }); } void _navigateBackToExplorer() { setState(() { _showDescription = false; + _selectedTemplate = null; }); } @override Widget build(BuildContext context) { return Scaffold( - body: _showDescription - ? DescriptionPage(onBack: _navigateBackToExplorer) + body: _showDescription + ? DescriptionPage( + template: _selectedTemplate!, + onBack: _navigateBackToExplorer, + ) : Column( children: [ const SizedBox( - height: 60, + height: 60, child: ExplorerHeader(), ), Expanded( From 1ecaa58a008f13a1a460e2aea863404ac0eda636 Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Mon, 21 Apr 2025 19:30:38 +0530 Subject: [PATCH 05/25] Add RequestCard, rename MethodPane to RequestsPane, update DescriptionBody to display scrollable request list - Created requests_card.dart with RequestCard widget to display request title. - Renamed method_pane.dart to request_pane.dart and MethodPane to RequestsPane, implementing scrollable ListView of RequestCard. Signed-off-by: Balasubramaniam12007 --- .../description/description_body.dart | 14 +++++--- .../description/description_page.dart | 4 +-- .../explorer/description/method_pane.dart | 27 ---------------- .../explorer/description/request_pane.dart | 28 ++++++++++++++++ .../explorer/description/requests_card.dart | 32 +++++++++++++++++++ 5 files changed, 72 insertions(+), 33 deletions(-) delete mode 100644 lib/screens/explorer/description/method_pane.dart create mode 100644 lib/screens/explorer/description/request_pane.dart create mode 100644 lib/screens/explorer/description/requests_card.dart diff --git a/lib/screens/explorer/description/description_body.dart b/lib/screens/explorer/description/description_body.dart index 27b0b9c60..cd0d2ce58 100644 --- a/lib/screens/explorer/description/description_body.dart +++ b/lib/screens/explorer/description/description_body.dart @@ -1,9 +1,15 @@ import 'package:flutter/material.dart'; -import 'method_pane.dart'; +import 'package:apidash/models/models.dart'; +import 'request_pane.dart'; import 'description_pane.dart'; class DescriptionBody extends StatelessWidget { - const DescriptionBody({super.key}); + final ApiTemplate template; + + const DescriptionBody({ + super.key, + required this.template, + }); @override Widget build(BuildContext context) { @@ -12,8 +18,8 @@ class DescriptionBody extends StatelessWidget { child: Row( children: [ SizedBox( - width: MediaQuery.of(context).size.width * 0.3, - child: const MethodPane(), + width: MediaQuery.of(context).size.width * 0.3, + child: RequestsPane(requests: template.requests), ), const Expanded( child: DescriptionPane(), diff --git a/lib/screens/explorer/description/description_page.dart b/lib/screens/explorer/description/description_page.dart index 3cbb4ee3f..49eb088e9 100644 --- a/lib/screens/explorer/description/description_page.dart +++ b/lib/screens/explorer/description/description_page.dart @@ -21,8 +21,8 @@ class DescriptionPage extends StatelessWidget { info: template.info, onBack: onBack, ), - const Expanded( - child: DescriptionBody(), + Expanded( + child: DescriptionBody(template: template), ), ], ); diff --git a/lib/screens/explorer/description/method_pane.dart b/lib/screens/explorer/description/method_pane.dart deleted file mode 100644 index 8ba3f6bb0..000000000 --- a/lib/screens/explorer/description/method_pane.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; - -class MethodPane extends StatelessWidget { - const MethodPane({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - color: Theme.of(context).colorScheme.surface, - padding: const EdgeInsets.all(12.0), - child: const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Method Pane', // Placeholder - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ), - SizedBox(height: 8), - Text( - 'This pane will display API method details (e.g., GET, POST).', // Placeholder - style: TextStyle(fontSize: 14), - ), - ], - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/explorer/description/request_pane.dart b/lib/screens/explorer/description/request_pane.dart new file mode 100644 index 000000000..a7955e3fe --- /dev/null +++ b/lib/screens/explorer/description/request_pane.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'requests_card.dart'; + +class RequestsPane extends StatelessWidget { + final List requests; + + const RequestsPane({ + super.key, + required this.requests, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: ListView.builder( + itemCount: requests.length, + itemBuilder: (context, index) { + final request = requests[index]; + return RequestCard( + title: request.name, + ); + }, + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/explorer/description/requests_card.dart b/lib/screens/explorer/description/requests_card.dart new file mode 100644 index 000000000..eed9039c5 --- /dev/null +++ b/lib/screens/explorer/description/requests_card.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; + +class RequestCard extends StatelessWidget { + final String title; + + const RequestCard({super.key, required this.title}); + + @override + Widget build(BuildContext context) { + return Card( + margin: const EdgeInsets.symmetric(vertical: 4.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + elevation: 0, + color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.3), + child: SizedBox( + width: double.infinity, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + child: Text( + title, + style: TextStyle( + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ), + ); + } +} \ No newline at end of file From b47f213672fd2ad18cd94a4403cc988c39678bb2 Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Tue, 22 Apr 2025 19:10:52 +0530 Subject: [PATCH 06/25] feat: add UrlCard widget displaying method, non-editable URL, and Send button placeholder feat: integrate UrlCard into DescriptionPane with request selection support fix: adjust MethodWidget to show HTTPVerb as GET, PUT instead of enum names --- .../explorer/common_widgets/url_card.dart | 73 +++++++++++++++++++ .../description/description_body.dart | 23 +++++- .../description/description_pane.dart | 18 +++-- .../explorer/description/request_pane.dart | 24 +++++- .../explorer/description/requests_card.dart | 34 ++++++--- 5 files changed, 147 insertions(+), 25 deletions(-) create mode 100644 lib/screens/explorer/common_widgets/url_card.dart diff --git a/lib/screens/explorer/common_widgets/url_card.dart b/lib/screens/explorer/common_widgets/url_card.dart new file mode 100644 index 000000000..c5a8eb833 --- /dev/null +++ b/lib/screens/explorer/common_widgets/url_card.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; + +class MethodWidget extends StatelessWidget { + final String method; + + const MethodWidget({super.key, required this.method}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(8.0), + child: Text( + method, + style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.green), + ), + ); + } +} + +class ImportButton extends StatelessWidget { + const ImportButton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: const Text( + 'Import', + style: TextStyle(color: Colors.blue), + ), + ); + } +} + +class UrlCard extends StatelessWidget { + final String? url; + final String method; + + const UrlCard({super.key, this.url, this.method = 'GET'}); + + @override + Widget build(BuildContext context) { + return Card( + color: Colors.transparent, + elevation: 0, + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + ), + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 20), + child: Row( + children: [ + MethodWidget(method: method), + const SizedBox(width: 5), + Expanded( + child: Text( + url ?? '', + style: const TextStyle(color: Colors.blue), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + const SizedBox(width: 20), + const ImportButton(), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/explorer/description/description_body.dart b/lib/screens/explorer/description/description_body.dart index cd0d2ce58..b23d5f84d 100644 --- a/lib/screens/explorer/description/description_body.dart +++ b/lib/screens/explorer/description/description_body.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; import 'package:apidash/models/models.dart'; +import 'package:apidash_core/apidash_core.dart'; import 'request_pane.dart'; import 'description_pane.dart'; -class DescriptionBody extends StatelessWidget { +class DescriptionBody extends StatefulWidget { final ApiTemplate template; const DescriptionBody({ @@ -11,6 +12,13 @@ class DescriptionBody extends StatelessWidget { required this.template, }); + @override + State createState() => _DescriptionBodyState(); +} + +class _DescriptionBodyState extends State { + RequestModel? _selectedRequest; + @override Widget build(BuildContext context) { return Container( @@ -19,10 +27,17 @@ class DescriptionBody extends StatelessWidget { children: [ SizedBox( width: MediaQuery.of(context).size.width * 0.3, - child: RequestsPane(requests: template.requests), + child: RequestsPane( + requests: widget.template.requests, + onRequestSelected: (request) { + setState(() { + _selectedRequest = request; + }); + }, + ), ), - const Expanded( - child: DescriptionPane(), + Expanded( + child: DescriptionPane(selectedRequest: _selectedRequest), ), ], ), diff --git a/lib/screens/explorer/description/description_pane.dart b/lib/screens/explorer/description/description_pane.dart index 3b94283ee..797dc11b4 100644 --- a/lib/screens/explorer/description/description_pane.dart +++ b/lib/screens/explorer/description/description_pane.dart @@ -1,17 +1,21 @@ import 'package:flutter/material.dart'; +import 'package:apidash_core/apidash_core.dart'; +import '../common_widgets/url_card.dart'; +import 'package:apidash/models/models.dart'; class DescriptionPane extends StatelessWidget { - const DescriptionPane({super.key}); + final RequestModel? selectedRequest; + + const DescriptionPane({super.key, this.selectedRequest}); @override Widget build(BuildContext context) { return Container( - color: Colors.white, - child: const Center( - child: Text( - 'Description Pane', - style: TextStyle(fontSize: 18), - ), + color: Theme.of(context).colorScheme.background, + child: Column( + children: [ + UrlCard(url: selectedRequest?.httpRequestModel?.url), + ], ), ); } diff --git a/lib/screens/explorer/description/request_pane.dart b/lib/screens/explorer/description/request_pane.dart index a7955e3fe..c2d0baebe 100644 --- a/lib/screens/explorer/description/request_pane.dart +++ b/lib/screens/explorer/description/request_pane.dart @@ -1,25 +1,43 @@ import 'package:flutter/material.dart'; import 'package:apidash_core/apidash_core.dart'; import 'requests_card.dart'; +import 'package:apidash/models/models.dart'; -class RequestsPane extends StatelessWidget { + +class RequestsPane extends StatefulWidget { final List requests; + final Function(RequestModel)? onRequestSelected; const RequestsPane({ super.key, required this.requests, + this.onRequestSelected, }); + @override + State createState() => _RequestsPaneState(); +} + +class _RequestsPaneState extends State { + RequestModel? _selectedRequest; + @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(16.0), child: ListView.builder( - itemCount: requests.length, + itemCount: widget.requests.length, itemBuilder: (context, index) { - final request = requests[index]; + final request = widget.requests[index]; return RequestCard( title: request.name, + isSelected: _selectedRequest == request, + onTap: () { + setState(() { + _selectedRequest = request; + }); + widget.onRequestSelected?.call(request); + }, ); }, ), diff --git a/lib/screens/explorer/description/requests_card.dart b/lib/screens/explorer/description/requests_card.dart index eed9039c5..8f92a79af 100644 --- a/lib/screens/explorer/description/requests_card.dart +++ b/lib/screens/explorer/description/requests_card.dart @@ -2,8 +2,15 @@ import 'package:flutter/material.dart'; class RequestCard extends StatelessWidget { final String title; + final bool isSelected; + final VoidCallback? onTap; - const RequestCard({super.key, required this.title}); + const RequestCard({ + super.key, + required this.title, + this.isSelected = false, + this.onTap, + }); @override Widget build(BuildContext context) { @@ -13,16 +20,21 @@ class RequestCard extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), elevation: 0, - color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.3), - child: SizedBox( - width: double.infinity, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), - child: Text( - title, - style: TextStyle( - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.onSurface, + color: isSelected + ? Theme.of(context).colorScheme.primary.withOpacity(0.2) + : Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.3), + child: InkWell( + onTap: onTap, + child: SizedBox( + width: double.infinity, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + child: Text( + title, + style: TextStyle( + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface, + ), ), ), ), From 38a90fb8df0d879bb4a1476ec30c9ab39b1d9a9e Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Tue, 22 Apr 2025 19:38:58 +0530 Subject: [PATCH 07/25] feat: add ExplorerSplitView for resizable request and description panes --- .../description/description_body.dart | 28 ++++----- .../description/explorer_split_view.dart | 62 +++++++++++++++++++ 2 files changed, 73 insertions(+), 17 deletions(-) create mode 100644 lib/screens/explorer/description/explorer_split_view.dart diff --git a/lib/screens/explorer/description/description_body.dart b/lib/screens/explorer/description/description_body.dart index b23d5f84d..e1296161e 100644 --- a/lib/screens/explorer/description/description_body.dart +++ b/lib/screens/explorer/description/description_body.dart @@ -3,6 +3,7 @@ import 'package:apidash/models/models.dart'; import 'package:apidash_core/apidash_core.dart'; import 'request_pane.dart'; import 'description_pane.dart'; +import 'explorer_split_view.dart'; class DescriptionBody extends StatefulWidget { final ApiTemplate template; @@ -23,23 +24,16 @@ class _DescriptionBodyState extends State { Widget build(BuildContext context) { return Container( color: Theme.of(context).colorScheme.background, - child: Row( - children: [ - SizedBox( - width: MediaQuery.of(context).size.width * 0.3, - child: RequestsPane( - requests: widget.template.requests, - onRequestSelected: (request) { - setState(() { - _selectedRequest = request; - }); - }, - ), - ), - Expanded( - child: DescriptionPane(selectedRequest: _selectedRequest), - ), - ], + child: ExplorerSplitView( + sidebarWidget: RequestsPane( + requests: widget.template.requests, + onRequestSelected: (request) { + setState(() { + _selectedRequest = request; + }); + }, + ), + mainWidget: DescriptionPane(selectedRequest: _selectedRequest), ), ); } diff --git a/lib/screens/explorer/description/explorer_split_view.dart b/lib/screens/explorer/description/explorer_split_view.dart new file mode 100644 index 000000000..09c4de333 --- /dev/null +++ b/lib/screens/explorer/description/explorer_split_view.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:multi_split_view/multi_split_view.dart'; + +class ExplorerSplitView extends StatefulWidget { + const ExplorerSplitView({ + super.key, + required this.sidebarWidget, + required this.mainWidget, + }); + + final Widget sidebarWidget; + final Widget mainWidget; + + @override + ExplorerSplitViewState createState() => ExplorerSplitViewState(); +} + +class ExplorerSplitViewState extends State { + final MultiSplitViewController _controller = MultiSplitViewController( + areas: [ + Area(id: "sidebar", min: 350, size: 400, max: 450), + Area(id: "main"), + ], + ); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return MultiSplitViewTheme( + data: MultiSplitViewThemeData( + dividerThickness: 3, + dividerPainter: DividerPainters.background( + color: Theme.of(context).colorScheme.surfaceContainer, + highlightedColor: Theme.of(context).colorScheme.surfaceContainerHighest, + animationEnabled: false, + ), + ), + child: MultiSplitView( + controller: _controller, + sizeOverflowPolicy: SizeOverflowPolicy.shrinkFirst, + sizeUnderflowPolicy: SizeUnderflowPolicy.stretchLast, + builder: (context, area) { + return switch (area.id) { + "sidebar" => widget.sidebarWidget, + "main" => widget.mainWidget, + _ => Container(), + }; + }, + ), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} \ No newline at end of file From bb4ceb7da5028f2539ea06a2103524a199502350 Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Tue, 22 Apr 2025 22:26:42 +0530 Subject: [PATCH 08/25] feat: create reusable MethodChip widget and integrate with UrlCard - Extracted HTTP method display into a reusable MethodChip widget - Styled MethodChip with method-specific colors and rounded borders - Updated UrlCard to use MethodChip instead of inline MethodWidget Signed-off-by: Balasubramaniam12007 --- .../explorer/common_widgets/method_chip.dart | 50 +++++++++++++++++++ .../explorer/common_widgets/url_card.dart | 26 +++------- .../description/description_pane.dart | 5 +- 3 files changed, 61 insertions(+), 20 deletions(-) create mode 100644 lib/screens/explorer/common_widgets/method_chip.dart diff --git a/lib/screens/explorer/common_widgets/method_chip.dart b/lib/screens/explorer/common_widgets/method_chip.dart new file mode 100644 index 000000000..591e83c02 --- /dev/null +++ b/lib/screens/explorer/common_widgets/method_chip.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; + +class MethodChip extends StatelessWidget { + final String method; + + const MethodChip({super.key, required this.method}); + + Color _getMethodColor() { + switch (method.toUpperCase()) { + case 'GET': + return kColorHttpMethodGet; + case 'HEAD': + return kColorHttpMethodHead; + case 'POST': + return kColorHttpMethodPost; + case 'PUT': + return kColorHttpMethodPut; + case 'PATCH': + return kColorHttpMethodPatch; + case 'DELETE': + return kColorHttpMethodDelete; + default: + return Colors.grey.shade700; + } + } + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 4.0), + decoration: BoxDecoration( + color: _getMethodColor().withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: _getMethodColor(), + width: 1.5, + ), + ), + child: Text( + method.toUpperCase(), + style: TextStyle( + fontWeight: FontWeight.bold, + color: _getMethodColor(), + fontSize: 12, + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/explorer/common_widgets/url_card.dart b/lib/screens/explorer/common_widgets/url_card.dart index c5a8eb833..7d36a5dc2 100644 --- a/lib/screens/explorer/common_widgets/url_card.dart +++ b/lib/screens/explorer/common_widgets/url_card.dart @@ -1,21 +1,5 @@ import 'package:flutter/material.dart'; - -class MethodWidget extends StatelessWidget { - final String method; - - const MethodWidget({super.key, required this.method}); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(8.0), - child: Text( - method, - style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.green), - ), - ); - } -} +import '../common_widgets/method_chip.dart'; class ImportButton extends StatelessWidget { const ImportButton({super.key}); @@ -36,7 +20,11 @@ class UrlCard extends StatelessWidget { final String? url; final String method; - const UrlCard({super.key, this.url, this.method = 'GET'}); + const UrlCard({ + super.key, + required this.url, + required this.method, + }); @override Widget build(BuildContext context) { @@ -53,7 +41,7 @@ class UrlCard extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 20), child: Row( children: [ - MethodWidget(method: method), + MethodChip(method: method), const SizedBox(width: 5), Expanded( child: Text( diff --git a/lib/screens/explorer/description/description_pane.dart b/lib/screens/explorer/description/description_pane.dart index 797dc11b4..23a94d37d 100644 --- a/lib/screens/explorer/description/description_pane.dart +++ b/lib/screens/explorer/description/description_pane.dart @@ -14,7 +14,10 @@ class DescriptionPane extends StatelessWidget { color: Theme.of(context).colorScheme.background, child: Column( children: [ - UrlCard(url: selectedRequest?.httpRequestModel?.url), + UrlCard( + url: selectedRequest?.httpRequestModel?.url, + method: selectedRequest?.httpRequestModel?.method.toString().split('.').last ?? 'GET', + ), ], ), ); From 369121bde8bf17ebb6d2df212cb8bace3ba9731a Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Tue, 22 Apr 2025 22:39:29 +0530 Subject: [PATCH 09/25] fix:reslove the assets bug --- pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pubspec.yaml b/pubspec.yaml index e73edd429..79eb9dacc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -93,4 +93,5 @@ dev_dependencies: flutter: uses-material-design: true assets: + - assets/ - lib/screens/explorer/api_templates/mock/ From 5db943589ccf330cae7018864879f96a49af4bda Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Sun, 27 Apr 2025 10:24:36 +0530 Subject: [PATCH 10/25] Enhance mock json data Signed-off-by: Balasubramaniam12007 --- .../api_templates/mock/blog_post.json | 183 ++++++++++-------- .../api_templates/mock/ecommerce.json | 130 +++++++++++++ 2 files changed, 237 insertions(+), 76 deletions(-) create mode 100644 lib/screens/explorer/api_templates/mock/ecommerce.json diff --git a/lib/screens/explorer/api_templates/mock/blog_post.json b/lib/screens/explorer/api_templates/mock/blog_post.json index f4425687c..6cbb823c0 100644 --- a/lib/screens/explorer/api_templates/mock/blog_post.json +++ b/lib/screens/explorer/api_templates/mock/blog_post.json @@ -1,82 +1,113 @@ { - "info": { - "title": "Blog Post API", - "description": "API for creating, retrieving, and deleting blog posts", - "tags": ["blog", "posts", "content"] - }, - "requests": [ - { - "id": "post_create", - "apiType": "rest", - "name": "Create a blog post", - "description": "Publishes a new blog post", - "httpRequestModel": { - "method": "post", - "url": "https://api.example.com/v1/posts", - "headers": [ - {"name": "Content-Type", "value": "application/json"} - ], - "params": [], - "isHeaderEnabledList": [true], - "isParamEnabledList": [], - "bodyContentType": "json", - "body": "{\"title\": \"My First Post\", \"content\": \"This is a blog post\", \"author\": \"Jane Doe\"}", - "query": null, - "formData": null + "info": { + "title": "Blog Post API", + "description": "API for managing blog posts", + "tags": ["blog", "posts"] + }, + "requests": [ + { + "id": "post_create", + "apiType": "rest", + "name": "Create post", + "description": "Create a new blog post", + "httpRequestModel": { + "method": "post", + "url": "https://api.example.com/v1/posts", + "headers": [ + {"name": "Content-Type", "value": "application/json"} + ], + "params": [], + "isHeaderEnabledList": [true], + "isParamEnabledList": [], + "bodyContentType": "json", + "body": "{\"title\":\"New Post\",\"content\":\"Content here\"}", + "query": null, + "formData": null + }, + "responseStatus": 201, + "message": "Post created", + "httpResponseModel": { + "statusCode": 201, + "headers": { + "Content-Type": "application/json", + "Content-Length": "89" }, - "responseStatus": null, - "message": null, - "httpResponseModel": null, - "isWorking": false, - "sendingTime": null + "requestHeaders": { + "Content-Type": "application/json" + }, + "body": "{\"id\":1,\"title\":\"New Post\",\"content\":\"Content here\",\"created_at\":\"2025-04-25T10:30:00Z\"}", + "formattedBody": "{\n \"id\": 1,\n \"title\": \"New Post\",\n \"content\": \"Content here\",\n \"created_at\": \"2025-04-25T10:30:00Z\"\n}", + "time": 240000 + }, + "isWorking": false, + "sendingTime": null + }, + { + "id": "post_get_all", + "apiType": "rest", + "name": "List posts", + "description": "Get all blog posts", + "httpRequestModel": { + "method": "get", + "url": "https://api.example.com/v1/posts", + "headers": [], + "params": [ + {"name": "limit", "value": "10"} + ], + "isHeaderEnabledList": [], + "isParamEnabledList": [true], + "bodyContentType": "json", + "body": null, + "query": "limit=10", + "formData": null }, - { - "id": "post_get_all", - "apiType": "rest", - "name": "Get all blog posts", - "description": "Retrieves a list of all blog posts", - "httpRequestModel": { - "method": "get", - "url": "https://api.example.com/v1/posts", - "headers": [], - "params": [], - "isHeaderEnabledList": [], - "isParamEnabledList": [], - "bodyContentType": "json", - "body": null, - "query": null, - "formData": null + "responseStatus": 200, + "message": "Success", + "httpResponseModel": { + "statusCode": 200, + "headers": { + "Content-Type": "application/json", + "Content-Length": "126" }, - "responseStatus": null, - "message": null, - "httpResponseModel": null, - "isWorking": false, - "sendingTime": null + "requestHeaders": {}, + "body": "{\"data\":[{\"id\":1,\"title\":\"New Post\"},{\"id\":2,\"title\":\"Another Post\"}],\"meta\":{\"total\":2}}", + "formattedBody": "{\n \"data\": [\n {\n \"id\": 1,\n \"title\": \"New Post\"\n },\n {\n \"id\": 2,\n \"title\": \"Another Post\"\n }\n ],\n \"meta\": {\n \"total\": 2\n }\n}", + "time": 150000 + }, + "isWorking": false, + "sendingTime": null + }, + { + "id": "post_delete", + "apiType": "rest", + "name": "Delete post", + "description": "Delete a post by ID", + "httpRequestModel": { + "method": "delete", + "url": "https://api.example.com/v1/posts/1", + "headers": [], + "params": [], + "isHeaderEnabledList": [], + "isParamEnabledList": [], + "bodyContentType": "json", + "body": null, + "query": null, + "formData": null }, - { - "id": "post_delete", - "apiType": "rest", - "name": "Delete a blog post", - "description": "Removes a blog post by ID", - "httpRequestModel": { - "method": "delete", - "url": "https://api.example.com/v1/posts/1", - "headers": [], - "params": [ - {"name": "id", "value": "1"} - ], - "isHeaderEnabledList": [], - "isParamEnabledList": [true], - "bodyContentType": "json", - "body": null, - "query": null, - "formData": null + "responseStatus": 204, + "message": "Deleted", + "httpResponseModel": { + "statusCode": 204, + "headers": { + "Content-Length": "0" }, - "responseStatus": null, - "message": null, - "httpResponseModel": null, - "isWorking": false, - "sendingTime": null - } - ] - } \ No newline at end of file + "requestHeaders": {}, + "body": "", + "formattedBody": "", + "time": 110000 + }, + "isWorking": false, + "sendingTime": null + } + ] +} \ No newline at end of file diff --git a/lib/screens/explorer/api_templates/mock/ecommerce.json b/lib/screens/explorer/api_templates/mock/ecommerce.json new file mode 100644 index 000000000..35a51f8bd --- /dev/null +++ b/lib/screens/explorer/api_templates/mock/ecommerce.json @@ -0,0 +1,130 @@ +{ + "info": { + "title": "E-commerce API", + "description": "API for managing products, orders, and customers in an e-commerce platform", + "tags": ["ecommerce", "products", "orders", "customers"] + }, + "requests": [ + { + "id": "order_get", + "apiType": "rest", + "name": "Get Order", + "description": "Retrieves details about a specific order", + "httpRequestModel": { + "method": "get", + "url": "https://api.example.com/v1/orders/ord_456", + "headers": [ + {"name": "Accept", "value": "application/json"}, + {"name": "Authorization", "value": "Bearer YOUR_TOKEN"} + ], + "params": [], + "isHeaderEnabledList": [true, true], + "isParamEnabledList": [], + "bodyContentType": "json", + "body": null, + "query": null, + "formData": null + }, + "responseStatus": 200, + "message": "Order retrieved successfully", + "httpResponseModel": { + "statusCode": 200, + "headers": { + "Content-Type": "application/json", + "Content-Length": "612" + }, + "requestHeaders": { + "Accept": "application/json", + "Authorization": "Bearer YOUR_TOKEN" + }, + "body": "{\n \"id\": \"ord_456\",\n \"customer_id\": \"cust_789\",\n \"status\": \"shipped\",\n \"items\": [\n {\n \"product_id\": \"prod_123\",\n \"name\": \"Wireless Earbuds\",\n \"quantity\": 2,\n \"unit_price\": 79.99,\n \"subtotal\": 159.98\n },\n {\n \"product_id\": \"prod_124\",\n \"name\": \"Smart Watch\",\n \"quantity\": 1,\n \"unit_price\": 129.99,\n \"subtotal\": 129.99\n }\n ],\n \"subtotal\": 289.97,\n \"tax\": 23.20,\n \"shipping\": 12.99,\n \"total\": 326.16,\n \"shipping_address\": {\n \"street\": \"123 Main St\",\n \"city\": \"Anytown\",\n \"state\": \"CA\",\n \"postal_code\": \"12345\",\n \"country\": \"US\"\n },\n \"tracking_number\": \"1ZW5Y9949045539359\",\n \"created_at\": \"2025-04-25T16:30:15Z\",\n \"updated_at\": \"2025-04-25T18:45:22Z\"\n}", + "formattedBody": "{\n \"id\": \"ord_456\",\n \"customer_id\": \"cust_789\",\n \"status\": \"shipped\",\n \"items\": [\n {\n \"product_id\": \"prod_123\",\n \"name\": \"Wireless Earbuds\",\n \"quantity\": 2,\n \"unit_price\": 79.99,\n \"subtotal\": 159.98\n },\n {\n \"product_id\": \"prod_124\",\n \"name\": \"Smart Watch\",\n \"quantity\": 1,\n \"unit_price\": 129.99,\n \"subtotal\": 129.99\n }\n ],\n \"subtotal\": 289.97,\n \"tax\": 23.20,\n \"shipping\": 12.99,\n \"total\": 326.16,\n \"shipping_address\": {\n \"street\": \"123 Main St\",\n \"city\": \"Anytown\",\n \"state\": \"CA\",\n \"postal_code\": \"12345\",\n \"country\": \"US\"\n },\n \"tracking_number\": \"1ZW5Y9949045539359\",\n \"created_at\": \"2025-04-25T16:30:15Z\",\n \"updated_at\": \"2025-04-25T18:45:22Z\"\n}", + "bodyBytes": null, + "time": 143000 + }, + "isWorking": false, + "sendingTime": null + }, + { + "id": "order_update_status", + "apiType": "rest", + "name": "Update Order Status", + "description": "Updates the status of an existing order", + "httpRequestModel": { + "method": "patch", + "url": "https://api.example.com/v1/orders/ordi_456/status", + "headers": [ + {"name": "Content-Type", "value": "application/json"}, + {"name": "Authorization", "value": "Bearer YOUR_TOKEN"} + ], + "params": [], + "isHeaderEnabledList": [true, true], + "isParamEnabledList": [], + "bodyContentType": "json", + "body": "{\n \"status\": \"delivered\",\n \"notes\": \"Delivered to customer's porch\"\n}", + "query": null, + "formData": null + }, + "responseStatus": 200, + "message": "Order status updated successfully", + "httpResponseModel": { + "statusCode": 200, + "headers": { + "Content-Type": "application/json", + "Content-Length": "128" + }, + "requestHeaders": { + "Content-Type": "application/json", + "Authorization": "Bearer YOUR_TOKEN" + }, + "body": "{\n \"id\": \"ord_456\",\n \"status\": \"delivered\",\n \"updated_at\": \"2025-04-26T10:15:30Z\",\n \"notes\": \"Delivered to customer's porch\"\n}", + "formattedBody": "{\n \"id\": \"ord_456\",\n \"status\": \"delivered\",\n \"updated_at\": \"2025-04-26T10:15:30Z\",\n \"notes\": \"Delivered to customer's porch\"\n}", + "bodyBytes": null, + "time": 185000 + }, + "isWorking": false, + "sendingTime": null + }, + { + "id": "customer_create", + "apiType": "rest", + "name": "Create Customer", + "description": "Registers a new customer in the system", + "httpRequestModel": { + "method": "post", + "url": "https://api.example.com/v1/customers", + "headers": [ + {"name": "Content-Type", "value": "application/json"}, + {"name": "Authorization", "value": "Bearer YOUR_TOKEN"} + ], + "params": [], + "isHeaderEnabledList": [true, true], + "isParamEnabledList": [], + "bodyContentType": "json", + "body": "{\n \"email\": \"jane.smith@example.com\",\n \"first_name\": \"Jane\",\n \"last_name\": \"Smith\",\n \"phone\": \"+1-555-123-4567\",\n \"addresses\": [\n {\n \"type\": \"shipping\",\n \"street\": \"123 Main St\",\n \"city\": \"Anytown\",\n \"state\": \"CA\",\n \"postal_code\": \"12345\",\n \"country\": \"US\"\n }\n ]\n}", + "query": null, + "formData": null + }, + "responseStatus": 201, + "message": "Customer created successfully", + "httpResponseModel": { + "statusCode": 201, + "headers": { + "Content-Type": "application/json", + "Content-Length": "294", + "Location": "https://api.example.com/v1/customers/cust_789" + }, + "requestHeaders": { + "Content-Type": "application/json", + "Authorization": "Bearer YOUR_TOKEN" + }, + "body": "{\n \"id\": \"cust_789\",\n \"email\": \"jane.smith@example.com\",\n \"first_name\": \"Jane\",\n \"last_name\": \"Smith\",\n \"phone\": \"+1-555-123-4567\",\n \"addresses\": [\n {\n \"type\": \"shipping\",\n \"street\": \"123 Main St\",\n \"city\": \"Anytown\",\n \"state\": \"CA\",\n \"postal_code\": \"12345\",\n \"country\": \"US\"\n }\n ],\n \"created_at\": \"2025-04-25T11:20:35Z\"\n}", + "formattedBody": "{\n \"id\": \"cust_789\",\n \"email\": \"jane.smith@example.com\",\n \"first_name\": \"Jane\",\n \"last_name\": \"Smith\",\n \"phone\": \"+1-555-123-4567\",\n \"addresses\": [\n {\n \"type\": \"shipping\",\n \"street\": \"123 Main St\",\n \"city\": \"Anytown\",\n \"state\": \"CA\",\n \"postal_code\": \"12345\",\n \"country\": \"US\"\n }\n ],\n \"created_at\": \"2025-04-25T11:20:35Z\"\n}", + "bodyBytes": null, + "time": 289000 + }, + "isWorking": false, + "sendingTime": null + } + ] +} \ No newline at end of file From 9ec3e555a2a260cfc17c77ed310eda4502db1ad3 Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Sun, 27 Apr 2025 15:44:33 +0530 Subject: [PATCH 11/25] fix bugs and refactor the files Signed-off-by: Balasubramaniam12007 --- .../api_templates/mock/event_api.json | 82 ------ .../explorer/api_templates/mock/pet_api.json | 162 +++++++++++ .../common_widgets/common_widgets.dart | 8 + .../common_widgets/response_card.dart | 271 ++++++++++++++++++ .../description/description_pane.dart | 56 +++- lib/screens/explorer/explorer_body.dart | 3 +- lib/screens/explorer/explorer_header.dart | 4 +- 7 files changed, 491 insertions(+), 95 deletions(-) delete mode 100644 lib/screens/explorer/api_templates/mock/event_api.json create mode 100644 lib/screens/explorer/api_templates/mock/pet_api.json create mode 100644 lib/screens/explorer/common_widgets/common_widgets.dart create mode 100644 lib/screens/explorer/common_widgets/response_card.dart diff --git a/lib/screens/explorer/api_templates/mock/event_api.json b/lib/screens/explorer/api_templates/mock/event_api.json deleted file mode 100644 index e61c1ddc7..000000000 --- a/lib/screens/explorer/api_templates/mock/event_api.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "info": { - "title": "Event Booking API", - "description": "API for booking and managing event tickets", - "tags": ["events", "bookings", "tickets"] - }, - "requests": [ - { - "id": "event_book", - "apiType": "rest", - "name": "Book an event ticket", - "description": "Books a ticket for a specific event", - "httpRequestModel": { - "method": "post", - "url": "https://api.example.com/v1/bookings", - "headers": [ - {"name": "Content-Type", "value": "application/json"} - ], - "params": [], - "isHeaderEnabledList": [true], - "isParamEnabledList": [], - "bodyContentType": "json", - "body": "{\"eventId\": \"evt456\", \"userId\": \"user789\", \"quantity\": 2}", - "query": null, - "formData": null - }, - "responseStatus": null, - "message": null, - "httpResponseModel": null, - "isWorking": false, - "sendingTime": null - }, - { - "id": "event_get_bookings", - "apiType": "rest", - "name": "Get all bookings", - "description": "Retrieves a list of all event bookings", - "httpRequestModel": { - "method": "get", - "url": "https://api.example.com/v1/bookings", - "headers": [], - "params": [], - "isHeaderEnabledList": [], - "isParamEnabledList": [], - "bodyContentType": "json", - "body": null, - "query": null, - "formData": null - }, - "responseStatus": null, - "message": null, - "httpResponseModel": null, - "isWorking": false, - "sendingTime": null - }, - { - "id": "event_cancel_booking", - "apiType": "rest", - "name": "Cancel a booking", - "description": "Cancels an event booking by ID", - "httpRequestModel": { - "method": "delete", - "url": "https://api.example.com/v1/bookings/123", - "headers": [], - "params": [ - {"name": "id", "value": "123"} - ], - "isHeaderEnabledList": [], - "isParamEnabledList": [true], - "bodyContentType": "json", - "body": null, - "query": null, - "formData": null - }, - "responseStatus": null, - "message": null, - "httpResponseModel": null, - "isWorking": false, - "sendingTime": null - } - ] - } \ No newline at end of file diff --git a/lib/screens/explorer/api_templates/mock/pet_api.json b/lib/screens/explorer/api_templates/mock/pet_api.json new file mode 100644 index 000000000..e39e19807 --- /dev/null +++ b/lib/screens/explorer/api_templates/mock/pet_api.json @@ -0,0 +1,162 @@ +{ + "info": { + "title": "Swagger Petstore", + "description": "API for managing pets in the pet store", + "tags": ["pets", "petstore", "api"] + }, + "requests": [ + { + "id": "list_pets", + "apiType": "rest", + "name": "List all pets", + "description": "Returns a paged array of pets", + "httpRequestModel": { + "method": "get", + "url": "http://petstore.swagger.io/v1/pets", + "headers": [ + {"name": "Accept", "value": "application/json"} + ], + "params": [ + {"name": "limit", "value": "10"} + ], + "isHeaderEnabledList": [true], + "isParamEnabledList": [true], + "bodyContentType": "json", + "body": null, + "query": "limit=10", + "formData": null + }, + "responseStatus": 200, + "message": "An paged array of pets", + "httpResponseModel": { + "statusCode": 200, + "headers": { + "Content-Type": "application/json", + "x-next": "/v1/pets?page=2", + "Content-Length": "157" + }, + "requestHeaders": { + "Accept": "application/json" + }, + "body": "[{\"id\":1,\"name\":\"Dog\",\"tag\":\"golden\"},{\"id\":2,\"name\":\"Cat\",\"tag\":\"siamese\"}]", + "formattedBody": "[\n {\n \"id\": 1,\n \"name\": \"Dog\",\n \"tag\": \"golden\"\n },\n {\n \"id\": 2,\n \"name\": \"Cat\",\n \"tag\": \"siamese\"\n }\n]", + "time": 120000 + }, + "isWorking": false, + "sendingTime": null + }, + { + "id": "create_pet", + "apiType": "rest", + "name": "Create a pet", + "description": "Creates a new pet in the store", + "httpRequestModel": { + "method": "post", + "url": "http://petstore.swagger.io/v1/pets", + "headers": [ + {"name": "Content-Type", "value": "application/json"}, + {"name": "Accept", "value": "application/json"} + ], + "params": [], + "isHeaderEnabledList": [true, true], + "isParamEnabledList": [], + "bodyContentType": "json", + "body": "{\"name\":\"Fluffy\",\"tag\":\"poodle\"}", + "query": null, + "formData": null + }, + "responseStatus": 201, + "message": "Pet created successfully", + "httpResponseModel": { + "statusCode": 201, + "headers": { + "Content-Type": "application/json", + "Content-Length": "0" + }, + "requestHeaders": { + "Content-Type": "application/json", + "Accept": "application/json" + }, + "body": "", + "formattedBody": "", + "time": 150000 + }, + "isWorking": false, + "sendingTime": null + }, + { + "id": "get_pet_by_id", + "apiType": "rest", + "name": "Info for a specific pet", + "description": "Returns details about a specific pet by ID", + "httpRequestModel": { + "method": "get", + "url": "http://petstore.swagger.io/v1/pets/1", + "headers": [ + {"name": "Accept", "value": "application/json"} + ], + "params": [], + "isHeaderEnabledList": [true], + "isParamEnabledList": [], + "bodyContentType": "json", + "body": null, + "query": null, + "formData": null + }, + "responseStatus": 200, + "message": "Expected response to a valid request", + "httpResponseModel": { + "statusCode": 200, + "headers": { + "Content-Type": "application/json", + "Content-Length": "45" + }, + "requestHeaders": { + "Accept": "application/json" + }, + "body": "{\"id\":1,\"name\":\"Dog\",\"tag\":\"golden\"}", + "formattedBody": "{\n \"id\": 1,\n \"name\": \"Dog\",\n \"tag\": \"golden\"\n}", + "time": 130000 + }, + "isWorking": false, + "sendingTime": null + }, + { + "id": "error_response", + "apiType": "rest", + "name": "Error example", + "description": "Example of an error response", + "httpRequestModel": { + "method": "get", + "url": "http://petstore.swagger.io/v1/pets/999", + "headers": [ + {"name": "Accept", "value": "application/json"} + ], + "params": [], + "isHeaderEnabledList": [true], + "isParamEnabledList": [], + "bodyContentType": "json", + "body": null, + "query": null, + "formData": null + }, + "responseStatus": 404, + "message": "Pet not found", + "httpResponseModel": { + "statusCode": 404, + "headers": { + "Content-Type": "application/json", + "Content-Length": "49" + }, + "requestHeaders": { + "Accept": "application/json" + }, + "body": "{\"code\":404,\"message\":\"Pet with ID 999 not found\"}", + "formattedBody": "{\n \"code\": 404,\n \"message\": \"Pet with ID 999 not found\"\n}", + "time": 110000 + }, + "isWorking": false, + "sendingTime": null + } + ] + } \ No newline at end of file diff --git a/lib/screens/explorer/common_widgets/common_widgets.dart b/lib/screens/explorer/common_widgets/common_widgets.dart new file mode 100644 index 000000000..15e0169b6 --- /dev/null +++ b/lib/screens/explorer/common_widgets/common_widgets.dart @@ -0,0 +1,8 @@ +export 'api_search_bar.dart'; +export 'url_card.dart'; +export 'response_card.dart'; +export 'method_chip.dart'; +export 'url_card.dart'; +export 'template_card.dart'; +export 'card_tittle.dart'; +export 'card_description.dart'; \ No newline at end of file diff --git a/lib/screens/explorer/common_widgets/response_card.dart b/lib/screens/explorer/common_widgets/response_card.dart new file mode 100644 index 000000000..72c9b98be --- /dev/null +++ b/lib/screens/explorer/common_widgets/response_card.dart @@ -0,0 +1,271 @@ +import 'package:flutter/material.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash/models/models.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; + +/// Displays a list of headers with enabled/disabled status +class RequestHeadersCard extends StatelessWidget { + final String title; + final List? headers; + final List? isEnabledList; + + const RequestHeadersCard({ + super.key, + required this.title, + this.headers, + this.isEnabledList, + }); + + @override + Widget build(BuildContext context) { + if (headers == null || headers!.isEmpty) return const SizedBox.shrink(); + + return StyledCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), + kVSpacer8, + ...List.generate( + headers!.length, + (i) => Padding( + padding: kPv2, + child: Row( + children: [ + Icon( + isEnabledList?[i] ?? true ? Icons.check_box : Icons.check_box_outline_blank, + size: 18, + color: Theme.of(context).colorScheme.primary, + ), + kHSpacer8, + Text(headers![i].name, style: const TextStyle(fontWeight: FontWeight.w500)), + kHSpacer8, + Text( + headers![i].value, + style: TextStyle(color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7)), + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +/// Displays a list of query parameters with enabled/disabled status +class RequestParamsCard extends StatelessWidget { + final String title; + final List? params; + final List? isEnabledList; + + const RequestParamsCard({ + super.key, + required this.title, + this.params, + this.isEnabledList, + }); + + @override + Widget build(BuildContext context) { + if (params == null || params!.isEmpty) return const SizedBox.shrink(); + + return StyledCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), + kVSpacer8, + ...List.generate( + params!.length, + (i) => Padding( + padding: kPv2, + child: Row( + children: [ + Icon( + isEnabledList?[i] ?? true ? Icons.check_box : Icons.check_box_outline_blank, + size: 18, + color: Theme.of(context).colorScheme.primary, + ), + kHSpacer8, + Text(params![i].name, style: const TextStyle(fontWeight: FontWeight.w500)), + kHSpacer8, + Text( + params![i].value, + style: TextStyle(color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7)), + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +/// Displays the request body with an optional content type label +class RequestBodyCard extends StatelessWidget { + final String title; + final String? body; + final String? contentType; + final bool showCopyButton; + + const RequestBodyCard({ + super.key, + required this.title, + this.body, + this.contentType, + this.showCopyButton = false, + }); + + @override + Widget build(BuildContext context) { + if (body == null || body!.isEmpty) return const SizedBox.shrink(); + + return StyledCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text(title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), + kHSpacer8, + if (contentType != null) + Chip( + label: Text( + contentType!, + style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onPrimary), + ), + backgroundColor: Theme.of(context).colorScheme.primary, + padding: EdgeInsets.zero, + visualDensity: VisualDensity.compact, + ), + ], + ), + kVSpacer8, + Container( + width: double.infinity, + padding: kP12, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerLowest, + borderRadius: kBorderRadius12, + ), + child: Text( + body!, + style: kCodeStyle.copyWith(color: Theme.of(context).colorScheme.onSurface), + ), + ), + ], + ), + ); + } +} + +/// A reusable card widget with consistent styling +class StyledCard extends StatelessWidget { + final Widget child; + + const StyledCard({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return Card( + color: Colors.transparent, + elevation: 0, + shape: RoundedRectangleBorder( + side: BorderSide(color: Theme.of(context).colorScheme.surfaceContainerHighest), + borderRadius: kBorderRadius12, + ), + child: Padding(padding: kP12, child: child), // Changed from kP16 to kP12 + ); + } +} + +/// Displays the response body along with summary information (status, message, time) +class ResponseBodyCard extends StatelessWidget { + final HttpResponseModel? httpResponseModel; + final int? responseStatus; + final String? message; + final String? body; + + const ResponseBodyCard({ + super.key, + this.httpResponseModel, + this.responseStatus, + this.message, + this.body, + }); + + Color _getStatusColor(int? status) { + if (status == null) return kColorStatusCodeDefault; + if (status >= 200 && status < 300) return kColorStatusCode200; + if (status >= 300 && status < 400) return kColorStatusCode300; + if (status >= 400 && status < 500) return kColorStatusCode400; + return kColorStatusCode500; + } + + @override + Widget build(BuildContext context) { + final statusCode = httpResponseModel?.statusCode ?? responseStatus; + final responseTime = httpResponseModel?.time?.inMilliseconds ?? 0; + final responseBody = body ?? httpResponseModel?.formattedBody ?? httpResponseModel?.body ?? ''; + final statusReason = statusCode != null ? kResponseCodeReasons[statusCode] ?? 'Unknown' : null; + + // Hide the card if there's no relevant data to display + if (statusCode == null && responseBody.isEmpty && (message == null || message!.isEmpty)) { + return const SizedBox.shrink(); + } + + return StyledCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Text(kLabelResponseBody, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), + kHSpacer8, + if (statusCode != null) + Chip( + label: Text('$statusCode${statusReason != null ? ' - $statusReason' : ''}', + style: const TextStyle(fontSize: 12, color: kColorWhite)), + backgroundColor: _getStatusColor(statusCode), + padding: EdgeInsets.zero, + visualDensity: VisualDensity.compact, + ), + const Spacer(), + if (responseTime > 0) + Text('${responseTime}ms', style: TextStyle( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7)) + ), + ], + ), + if (message != null && message!.isEmpty) kVSpacer8, + if (message != null && message!.isNotEmpty) ...[ + kVSpacer8, + Text(message!, style: TextStyle( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7)) + ), + ], + if (responseBody.isNotEmpty) ...[ + kVSpacer10, // Changed from kVSpacer12 to kVSpacer10 + Container( + width: double.infinity, + padding: kP12, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerLowest, + borderRadius: kBorderRadius12, + ), + child: Text( + responseBody, + style: kCodeStyle.copyWith(color: Theme.of(context).colorScheme.onSurface), + ), + ), + ], + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/explorer/description/description_pane.dart b/lib/screens/explorer/description/description_pane.dart index 23a94d37d..43f151d50 100644 --- a/lib/screens/explorer/description/description_pane.dart +++ b/lib/screens/explorer/description/description_pane.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:apidash_core/apidash_core.dart'; -import '../common_widgets/url_card.dart'; +import '../common_widgets/common_widgets.dart'; import 'package:apidash/models/models.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; class DescriptionPane extends StatelessWidget { final RequestModel? selectedRequest; @@ -10,15 +12,53 @@ class DescriptionPane extends StatelessWidget { @override Widget build(BuildContext context) { + final httpRequestModel = selectedRequest?.httpRequestModel; return Container( color: Theme.of(context).colorScheme.background, - child: Column( - children: [ - UrlCard( - url: selectedRequest?.httpRequestModel?.url, - method: selectedRequest?.httpRequestModel?.method.toString().split('.').last ?? 'GET', - ), - ], + child: SingleChildScrollView( + padding: kP12, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (selectedRequest?.name != null) ...[ + Text(selectedRequest!.name, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), + if (selectedRequest?.description != null) + Text(selectedRequest!.description, style: TextStyle( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7))), + kVSpacer16, + ], + UrlCard( + url: httpRequestModel?.url, + method: httpRequestModel?.method.toString().split('.').last ?? 'GET', + ), + kVSpacer16, + RequestHeadersCard( + title: kLabelHeaders, + headers: httpRequestModel?.headers, + isEnabledList: httpRequestModel?.isHeaderEnabledList, + ), + kVSpacer10, + RequestParamsCard( + title: kLabelQuery, + params: httpRequestModel?.params, + isEnabledList: httpRequestModel?.isParamEnabledList, + ), + kVSpacer10, + RequestBodyCard( + title: kLabelBody, + body: httpRequestModel?.body, + contentType: httpRequestModel?.bodyContentType?.toString().split('.').last, + ), + kVSpacer16, + ResponseBodyCard( + httpResponseModel: selectedRequest?.httpResponseModel, + responseStatus: selectedRequest?.responseStatus, + message: selectedRequest?.message, + body: selectedRequest?.httpResponseModel?.formattedBody ?? selectedRequest?.httpResponseModel?.body, + ), + ], + ), ), ); } diff --git a/lib/screens/explorer/explorer_body.dart b/lib/screens/explorer/explorer_body.dart index f5707f09e..0711728fc 100644 --- a/lib/screens/explorer/explorer_body.dart +++ b/lib/screens/explorer/explorer_body.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:apidash/services/services.dart'; -import 'common_widgets/template_card.dart'; -import 'common_widgets/api_search_bar.dart'; +import 'common_widgets/common_widgets.dart'; import 'package:apidash/models/models.dart'; class ExplorerBody extends StatefulWidget { diff --git a/lib/screens/explorer/explorer_header.dart b/lib/screens/explorer/explorer_header.dart index 5800e2270..afdd98442 100644 --- a/lib/screens/explorer/explorer_header.dart +++ b/lib/screens/explorer/explorer_header.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:apidash/screens/explorer/common_widgets/import.dart'; class ExplorerHeader extends StatelessWidget { const ExplorerHeader({super.key}); @@ -15,8 +14,7 @@ class ExplorerHeader extends StatelessWidget { const Text( 'API EXPLORER', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - const ImportButton(), + ) ], ), ); From 8d3f1182c9eefbdd1a6c3810b3412e7fe68c9846 Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Sun, 27 Apr 2025 16:33:53 +0530 Subject: [PATCH 12/25] Refactor chip components into reusable CustomChip - Created chip.dart with CustomChip to replace MethodChip and unify styling - Updated UrlCard and RequestBodyCard to use new chip implementations Signed-off-by: Balasubramaniam12007 --- lib/screens/explorer/common_widgets/chip.dart | 114 ++++++++++++++++++ .../common_widgets/common_widgets.dart | 5 +- .../common_widgets/response_card.dart | 44 ++----- .../explorer/common_widgets/url_card.dart | 13 +- 4 files changed, 135 insertions(+), 41 deletions(-) create mode 100644 lib/screens/explorer/common_widgets/chip.dart diff --git a/lib/screens/explorer/common_widgets/chip.dart b/lib/screens/explorer/common_widgets/chip.dart new file mode 100644 index 000000000..3acfeede4 --- /dev/null +++ b/lib/screens/explorer/common_widgets/chip.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import "package:apidash/consts.dart"; + +class CustomChip extends StatelessWidget { + final String label; + final Color? backgroundColor; + final Color? textColor; + final Color? borderColor; + final double fontSize; + final FontWeight fontWeight; + final EdgeInsets padding; + final VisualDensity visualDensity; + + const CustomChip({ + super.key, + required this.label, + this.backgroundColor, + this.textColor, + this.borderColor, + this.fontSize = 12, + this.fontWeight = FontWeight.bold, + this.padding = const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + this.visualDensity = VisualDensity.compact, + }); + + factory CustomChip.httpMethod(String method) { + Color color; + switch (method.toUpperCase()) { + case 'GET': + color = kColorHttpMethodGet; + break; + case 'HEAD': + color = kColorHttpMethodHead; + break; + case 'POST': + color = kColorHttpMethodPost; + break; + case 'PUT': + color = kColorHttpMethodPut; + break; + case 'PATCH': + color = kColorHttpMethodPatch; + break; + case 'DELETE': + color = kColorHttpMethodDelete; + break; + default: + color = Colors.grey.shade700; + } + return CustomChip( + label: method.toUpperCase(), + backgroundColor: color.withOpacity(0.1), + textColor: color, + borderColor: color, + ); + } + + factory CustomChip.statusCode(int status) { + Color color; + if (status >= 200 && status < 300) { + color = kColorStatusCode200; + } else if (status >= 300 && status < 400) { + color = kColorStatusCode300; + } else if (status >= 400 && status < 500) { + color = kColorStatusCode400; + } else if (status >= 500) { + color = kColorStatusCode500; + } else { + color = kColorStatusCodeDefault; + } + + String reason = kResponseCodeReasons[status] ?? ''; + String label = reason.isNotEmpty ? '$status - $reason' : '$status'; + return CustomChip( + label: label, + backgroundColor: color.withOpacity(0.1), + textColor: color, + borderColor: color, + ); + } + + factory CustomChip.contentType(String contentType) { + return CustomChip( + label: contentType, + backgroundColor: Colors.blue.withOpacity(0.1), + textColor: Colors.blue, + borderColor: Colors.blue, + ); + } + + @override + Widget build(BuildContext context) { + return Container( + padding: padding, + decoration: BoxDecoration( + color: backgroundColor ?? Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: borderColor ?? Theme.of(context).colorScheme.outline, + width: 1.5, + ), + ), + child: Text( + label, + style: TextStyle( + fontWeight: fontWeight, + color: textColor ?? Theme.of(context).colorScheme.onSurface, + fontSize: fontSize, + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/explorer/common_widgets/common_widgets.dart b/lib/screens/explorer/common_widgets/common_widgets.dart index 15e0169b6..b4cd03206 100644 --- a/lib/screens/explorer/common_widgets/common_widgets.dart +++ b/lib/screens/explorer/common_widgets/common_widgets.dart @@ -4,5 +4,6 @@ export 'response_card.dart'; export 'method_chip.dart'; export 'url_card.dart'; export 'template_card.dart'; -export 'card_tittle.dart'; -export 'card_description.dart'; \ No newline at end of file +export 'card_title.dart'; +export 'card_description.dart'; +export 'chip.dart'; \ No newline at end of file diff --git a/lib/screens/explorer/common_widgets/response_card.dart b/lib/screens/explorer/common_widgets/response_card.dart index 72c9b98be..028a0f0bf 100644 --- a/lib/screens/explorer/common_widgets/response_card.dart +++ b/lib/screens/explorer/common_widgets/response_card.dart @@ -3,6 +3,7 @@ import 'package:apidash_core/apidash_core.dart'; import 'package:apidash/models/models.dart'; import 'package:apidash/consts.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; +import 'chip.dart'; /// Displays a list of headers with enabled/disabled status class RequestHeadersCard extends StatelessWidget { @@ -11,9 +12,9 @@ class RequestHeadersCard extends StatelessWidget { final List? isEnabledList; const RequestHeadersCard({ - super.key, - required this.title, - this.headers, + super.key, + required this.title, + this.headers, this.isEnabledList, }); @@ -62,9 +63,9 @@ class RequestParamsCard extends StatelessWidget { final List? isEnabledList; const RequestParamsCard({ - super.key, - required this.title, - this.params, + super.key, + required this.title, + this.params, this.isEnabledList, }); @@ -134,15 +135,7 @@ class RequestBodyCard extends StatelessWidget { Text(title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), kHSpacer8, if (contentType != null) - Chip( - label: Text( - contentType!, - style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onPrimary), - ), - backgroundColor: Theme.of(context).colorScheme.primary, - padding: EdgeInsets.zero, - visualDensity: VisualDensity.compact, - ), + CustomChip.contentType(contentType!), ], ), kVSpacer8, @@ -179,7 +172,7 @@ class StyledCard extends StatelessWidget { side: BorderSide(color: Theme.of(context).colorScheme.surfaceContainerHighest), borderRadius: kBorderRadius12, ), - child: Padding(padding: kP12, child: child), // Changed from kP16 to kP12 + child: Padding(padding: kP12, child: child), ); } } @@ -199,20 +192,11 @@ class ResponseBodyCard extends StatelessWidget { this.body, }); - Color _getStatusColor(int? status) { - if (status == null) return kColorStatusCodeDefault; - if (status >= 200 && status < 300) return kColorStatusCode200; - if (status >= 300 && status < 400) return kColorStatusCode300; - if (status >= 400 && status < 500) return kColorStatusCode400; - return kColorStatusCode500; - } - @override Widget build(BuildContext context) { final statusCode = httpResponseModel?.statusCode ?? responseStatus; final responseTime = httpResponseModel?.time?.inMilliseconds ?? 0; final responseBody = body ?? httpResponseModel?.formattedBody ?? httpResponseModel?.body ?? ''; - final statusReason = statusCode != null ? kResponseCodeReasons[statusCode] ?? 'Unknown' : null; // Hide the card if there's no relevant data to display if (statusCode == null && responseBody.isEmpty && (message == null || message!.isEmpty)) { @@ -228,13 +212,7 @@ class ResponseBodyCard extends StatelessWidget { const Text(kLabelResponseBody, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), kHSpacer8, if (statusCode != null) - Chip( - label: Text('$statusCode${statusReason != null ? ' - $statusReason' : ''}', - style: const TextStyle(fontSize: 12, color: kColorWhite)), - backgroundColor: _getStatusColor(statusCode), - padding: EdgeInsets.zero, - visualDensity: VisualDensity.compact, - ), + CustomChip.statusCode(statusCode), const Spacer(), if (responseTime > 0) Text('${responseTime}ms', style: TextStyle( @@ -250,7 +228,7 @@ class ResponseBodyCard extends StatelessWidget { ), ], if (responseBody.isNotEmpty) ...[ - kVSpacer10, // Changed from kVSpacer12 to kVSpacer10 + kVSpacer10, Container( width: double.infinity, padding: kP12, diff --git a/lib/screens/explorer/common_widgets/url_card.dart b/lib/screens/explorer/common_widgets/url_card.dart index 7d36a5dc2..5640c8587 100644 --- a/lib/screens/explorer/common_widgets/url_card.dart +++ b/lib/screens/explorer/common_widgets/url_card.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import '../common_widgets/method_chip.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; // Ensure this is imported +import 'chip.dart'; class ImportButton extends StatelessWidget { const ImportButton({super.key}); @@ -21,8 +22,8 @@ class UrlCard extends StatelessWidget { final String method; const UrlCard({ - super.key, - required this.url, + super.key, + required this.url, required this.method, }); @@ -41,8 +42,8 @@ class UrlCard extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 20), child: Row( children: [ - MethodChip(method: method), - const SizedBox(width: 5), + CustomChip.httpMethod(method), + kHSpacer10, Expanded( child: Text( url ?? '', @@ -51,7 +52,7 @@ class UrlCard extends StatelessWidget { maxLines: 1, ), ), - const SizedBox(width: 20), + kHSpacer20, const ImportButton(), ], ), From 783347231dc3f95138e7e1c2bbe66f94d28e86af Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Sun, 27 Apr 2025 18:22:26 +0530 Subject: [PATCH 13/25] feat: add CustomChip.tag factory method and update TemplateCard tag display - Added CustomChip.tag factory method in chip.dart to centralize tag styling. - Updated TemplateCard to use CustomChip.tag for displaying tags in a single line. Signed-off-by: Balasubramaniam12007 --- lib/screens/explorer/common_widgets/chip.dart | 11 ++++ .../common_widgets/common_widgets.dart | 1 - .../explorer/common_widgets/method_chip.dart | 50 ------------------- .../common_widgets/template_card.dart | 9 +++- lib/screens/explorer/explorer_body.dart | 4 +- 5 files changed, 21 insertions(+), 54 deletions(-) delete mode 100644 lib/screens/explorer/common_widgets/method_chip.dart diff --git a/lib/screens/explorer/common_widgets/chip.dart b/lib/screens/explorer/common_widgets/chip.dart index 3acfeede4..5cde97a95 100644 --- a/lib/screens/explorer/common_widgets/chip.dart +++ b/lib/screens/explorer/common_widgets/chip.dart @@ -89,6 +89,17 @@ class CustomChip extends StatelessWidget { ); } + factory CustomChip.tag(String tag, ColorScheme colorScheme) { + return CustomChip( + label: tag, + fontSize: 10, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + backgroundColor: colorScheme.primary.withOpacity(0.1), + textColor: colorScheme.primary, + borderColor: colorScheme.primary.withOpacity(0.3), + ); + } + @override Widget build(BuildContext context) { return Container( diff --git a/lib/screens/explorer/common_widgets/common_widgets.dart b/lib/screens/explorer/common_widgets/common_widgets.dart index b4cd03206..c25d573cb 100644 --- a/lib/screens/explorer/common_widgets/common_widgets.dart +++ b/lib/screens/explorer/common_widgets/common_widgets.dart @@ -1,7 +1,6 @@ export 'api_search_bar.dart'; export 'url_card.dart'; export 'response_card.dart'; -export 'method_chip.dart'; export 'url_card.dart'; export 'template_card.dart'; export 'card_title.dart'; diff --git a/lib/screens/explorer/common_widgets/method_chip.dart b/lib/screens/explorer/common_widgets/method_chip.dart deleted file mode 100644 index 591e83c02..000000000 --- a/lib/screens/explorer/common_widgets/method_chip.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:apidash_design_system/apidash_design_system.dart'; - -class MethodChip extends StatelessWidget { - final String method; - - const MethodChip({super.key, required this.method}); - - Color _getMethodColor() { - switch (method.toUpperCase()) { - case 'GET': - return kColorHttpMethodGet; - case 'HEAD': - return kColorHttpMethodHead; - case 'POST': - return kColorHttpMethodPost; - case 'PUT': - return kColorHttpMethodPut; - case 'PATCH': - return kColorHttpMethodPatch; - case 'DELETE': - return kColorHttpMethodDelete; - default: - return Colors.grey.shade700; - } - } - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 4.0), - decoration: BoxDecoration( - color: _getMethodColor().withOpacity(0.1), - borderRadius: BorderRadius.circular(10), - border: Border.all( - color: _getMethodColor(), - width: 1.5, - ), - ), - child: Text( - method.toUpperCase(), - style: TextStyle( - fontWeight: FontWeight.bold, - color: _getMethodColor(), - fontSize: 12, - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/explorer/common_widgets/template_card.dart b/lib/screens/explorer/common_widgets/template_card.dart index 830508dfc..a8337166d 100644 --- a/lib/screens/explorer/common_widgets/template_card.dart +++ b/lib/screens/explorer/common_widgets/template_card.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:apidash/models/models.dart'; import 'card_title.dart'; import 'card_description.dart'; +import 'chip.dart'; class TemplateCard extends StatelessWidget { final ApiTemplate template; @@ -27,7 +28,7 @@ class TemplateCard extends StatelessWidget { ), ), color: colorScheme.surface, - elevation: 0.5, + elevation: 0.8, child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(16), @@ -46,6 +47,12 @@ class TemplateCard extends StatelessWidget { description: template.info.description.isEmpty ? 'No description' : template.info.description, maxLines: 2, ), + const SizedBox(height: 10), + Wrap( + spacing: 8, + runSpacing: 4, + children: template.info.tags.map((tag) => CustomChip.tag(tag, colorScheme)).toList(), + ), ], ), ), diff --git a/lib/screens/explorer/explorer_body.dart b/lib/screens/explorer/explorer_body.dart index 0711728fc..136fce570 100644 --- a/lib/screens/explorer/explorer_body.dart +++ b/lib/screens/explorer/explorer_body.dart @@ -64,8 +64,8 @@ class _ExplorerBodyState extends State { return GridView.builder( padding: const EdgeInsets.all(12), gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 300, - childAspectRatio: 1.3, + maxCrossAxisExtent: 384, + childAspectRatio: 1.6, crossAxisSpacing: 12, mainAxisSpacing: 12, ), From e862c13bae1a7ea6fe712f94141f41b7cebb96f5 Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Sun, 27 Apr 2025 20:08:24 +0530 Subject: [PATCH 14/25] feat: add support to import requests from templates and optimize performance - Created import.dart to extract HttpRequestModel from RequestModel for importing requests. - Modified description_pane.dart to pass RequestModel to UrlCard for integration. - Updated url_card.dart to include an Import button supporting request addition via CollectionStateNotifier. - Optimized the import process for better performance and null safety. Signed-off-by: Balasubramaniam12007 --- .../explorer/common_widgets/url_card.dart | 54 +++++++++++-------- .../description/description_pane.dart | 3 +- lib/screens/explorer/import.dart | 26 +++++++++ 3 files changed, 58 insertions(+), 25 deletions(-) create mode 100644 lib/screens/explorer/import.dart diff --git a/lib/screens/explorer/common_widgets/url_card.dart b/lib/screens/explorer/common_widgets/url_card.dart index 5640c8587..4c95b21f7 100644 --- a/lib/screens/explorer/common_widgets/url_card.dart +++ b/lib/screens/explorer/common_widgets/url_card.dart @@ -1,34 +1,27 @@ import 'package:flutter/material.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; // Ensure this is imported +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash/models/models.dart'; import 'chip.dart'; +import '../import.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; -class ImportButton extends StatelessWidget { - const ImportButton({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - child: const Text( - 'Import', - style: TextStyle(color: Colors.blue), - ), - ); - } -} - -class UrlCard extends StatelessWidget { - final String? url; - final String method; +class UrlCard extends ConsumerWidget { + final RequestModel? requestModel; const UrlCard({ super.key, - required this.url, - required this.method, + required this.requestModel, }); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final importedData = importRequestData(requestModel); + final httpRequestModel = importedData.httpRequestModel; + final url = httpRequestModel?.url ?? ''; + final method = httpRequestModel?.method.toString().split('.').last.toUpperCase() ?? 'GET'; + return Card( color: Colors.transparent, elevation: 0, @@ -46,14 +39,29 @@ class UrlCard extends StatelessWidget { kHSpacer10, Expanded( child: Text( - url ?? '', + url, style: const TextStyle(color: Colors.blue), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), kHSpacer20, - const ImportButton(), + ElevatedButton( + onPressed: () { + if (httpRequestModel != null) { + ref.read(collectionStateNotifierProvider.notifier).addRequestModel( + httpRequestModel, + name: requestModel?.name ?? 'Imported Request', + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of(context).colorScheme.onPrimary, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + child: const Text('Import'), + ), ], ), ), diff --git a/lib/screens/explorer/description/description_pane.dart b/lib/screens/explorer/description/description_pane.dart index 43f151d50..f294cd547 100644 --- a/lib/screens/explorer/description/description_pane.dart +++ b/lib/screens/explorer/description/description_pane.dart @@ -29,8 +29,7 @@ class DescriptionPane extends StatelessWidget { kVSpacer16, ], UrlCard( - url: httpRequestModel?.url, - method: httpRequestModel?.method.toString().split('.').last ?? 'GET', + requestModel: selectedRequest, ), kVSpacer16, RequestHeadersCard( diff --git a/lib/screens/explorer/import.dart b/lib/screens/explorer/import.dart new file mode 100644 index 000000000..d54362c9c --- /dev/null +++ b/lib/screens/explorer/import.dart @@ -0,0 +1,26 @@ +import 'package:apidash/models/models.dart'; +import 'package:apidash_core/apidash_core.dart'; + +class ImportedRequestData { + final HttpRequestModel? httpRequestModel; + + ImportedRequestData(this.httpRequestModel); + + // Static default instance for null cases + static final ImportedRequestData _default = ImportedRequestData( + HttpRequestModel( + url: '', + method: HTTPVerb.get, + headers: [], + params: [], + ), + ); + + // Factory constructor to return default instance if null + factory ImportedRequestData.empty() => _default; +} + +ImportedRequestData importRequestData(RequestModel? requestModel) { + final httpRequestModel = requestModel?.httpRequestModel ?? HttpRequestModel(); + return ImportedRequestData(httpRequestModel); +} \ No newline at end of file From 0bfdc19f22b0851911b0263ebe4aea7dcf55a167 Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Mon, 28 Apr 2025 11:22:18 +0530 Subject: [PATCH 15/25] Add navigation to HomePage after importing request from ExplorerPage --- lib/screens/explorer/common_widgets/url_card.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/screens/explorer/common_widgets/url_card.dart b/lib/screens/explorer/common_widgets/url_card.dart index 4c95b21f7..3e0e9c533 100644 --- a/lib/screens/explorer/common_widgets/url_card.dart +++ b/lib/screens/explorer/common_widgets/url_card.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:apidash_design_system/apidash_design_system.dart'; // Ensure this is imported +import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash/models/models.dart'; import 'chip.dart'; @@ -53,6 +53,7 @@ class UrlCard extends ConsumerWidget { httpRequestModel, name: requestModel?.name ?? 'Imported Request', ); + ref.read(navRailIndexStateProvider.notifier).state = 0; // Navigate to HomePage ind 0 } }, style: ElevatedButton.styleFrom( From f1e744640dcbb0c74de5293bba2a729d327b051e Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Mon, 28 Apr 2025 11:33:55 +0530 Subject: [PATCH 16/25] Add SnackBar notification for successful request import in UrlCard --- lib/screens/explorer/common_widgets/url_card.dart | 6 ++++++ lib/screens/explorer/explorer_page.dart | 11 ++--------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/screens/explorer/common_widgets/url_card.dart b/lib/screens/explorer/common_widgets/url_card.dart index 3e0e9c533..251945bb2 100644 --- a/lib/screens/explorer/common_widgets/url_card.dart +++ b/lib/screens/explorer/common_widgets/url_card.dart @@ -52,6 +52,12 @@ class UrlCard extends ConsumerWidget { ref.read(collectionStateNotifierProvider.notifier).addRequestModel( httpRequestModel, name: requestModel?.name ?? 'Imported Request', + ); + ScaffoldMessenger.of(context).showSnackBar( //SnackBar notification + SnackBar( + content: Text('Request "${requestModel?.name ?? 'Imported Request'}" imported successfully'), + duration: const Duration(seconds: 2), + ), ); ref.read(navRailIndexStateProvider.notifier).state = 0; // Navigate to HomePage ind 0 } diff --git a/lib/screens/explorer/explorer_page.dart b/lib/screens/explorer/explorer_page.dart index 6778d416c..506471bd7 100644 --- a/lib/screens/explorer/explorer_page.dart +++ b/lib/screens/explorer/explorer_page.dart @@ -39,15 +39,8 @@ class _ExplorerPageState extends State { ) : Column( children: [ - const SizedBox( - height: 60, - child: ExplorerHeader(), - ), - Expanded( - child: ExplorerBody( - onCardTap: _navigateToDescription, - ), - ), + const SizedBox(height: 60, child: ExplorerHeader()), + Expanded(child: ExplorerBody(onCardTap: _navigateToDescription)), ], ), ); From 713826ba92ede66c58a1339fb5e0c0111231781f Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Mon, 28 Apr 2025 11:46:35 +0530 Subject: [PATCH 17/25] resuse the getSnackBar widget --- lib/screens/explorer/common_widgets/url_card.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/screens/explorer/common_widgets/url_card.dart b/lib/screens/explorer/common_widgets/url_card.dart index 251945bb2..6eaff222f 100644 --- a/lib/screens/explorer/common_widgets/url_card.dart +++ b/lib/screens/explorer/common_widgets/url_card.dart @@ -53,10 +53,11 @@ class UrlCard extends ConsumerWidget { httpRequestModel, name: requestModel?.name ?? 'Imported Request', ); - ScaffoldMessenger.of(context).showSnackBar( //SnackBar notification - SnackBar( - content: Text('Request "${requestModel?.name ?? 'Imported Request'}" imported successfully'), - duration: const Duration(seconds: 2), + ScaffoldMessenger.of(context).showSnackBar( //SnackBar notification + getSnackBar( + 'Request "${requestModel?.name ?? 'Imported Request'}" imported successfully', + small: false, + color: Theme.of(context).colorScheme.primary, ), ); ref.read(navRailIndexStateProvider.notifier).state = 0; // Navigate to HomePage ind 0 From 10dc7a6e6f9722b659024280c5af8776edc6fd04 Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Mon, 28 Apr 2025 12:50:02 +0530 Subject: [PATCH 18/25] (fix)Responsive TemplateCard Ensured consistent width across cards with SizedBox Maintained 3:5:2 height ratio and responsiveness Signed-off-by: Balasubramaniam12007 --- .../common_widgets/card_description.dart | 20 ++-- .../common_widgets/template_card.dart | 91 ++++++++++++------- lib/screens/explorer/explorer_body.dart | 31 ++++--- 3 files changed, 82 insertions(+), 60 deletions(-) diff --git a/lib/screens/explorer/common_widgets/card_description.dart b/lib/screens/explorer/common_widgets/card_description.dart index ae38ac8ba..dac47d9c8 100644 --- a/lib/screens/explorer/common_widgets/card_description.dart +++ b/lib/screens/explorer/common_widgets/card_description.dart @@ -7,7 +7,7 @@ class CardDescription extends StatelessWidget { const CardDescription({ Key? key, required this.description, - this.maxLines = 2, + this.maxLines = 5, }) : super(key: key); @override @@ -15,23 +15,21 @@ class CardDescription extends StatelessWidget { final theme = Theme.of(context); return SizedBox( - height: 80, + width: double.infinity, child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: theme.colorScheme.surfaceVariant.withOpacity(0.3), borderRadius: BorderRadius.circular(8), ), - child: Center( - child: Text( - description.isEmpty ? 'No description' : description, - textAlign: TextAlign.left, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurface.withOpacity(0.7), - ), - maxLines: maxLines, - overflow: TextOverflow.ellipsis, + child: Text( + description.isEmpty ? 'No description' : description, + textAlign: TextAlign.left, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface, ), + maxLines: maxLines, + overflow: TextOverflow.ellipsis, ), ), ); diff --git a/lib/screens/explorer/common_widgets/template_card.dart b/lib/screens/explorer/common_widgets/template_card.dart index a8337166d..338ca9ad0 100644 --- a/lib/screens/explorer/common_widgets/template_card.dart +++ b/lib/screens/explorer/common_widgets/template_card.dart @@ -18,42 +18,63 @@ class TemplateCard extends StatelessWidget { Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; - return Card( - margin: const EdgeInsets.all(8), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - side: BorderSide( - color: colorScheme.outline.withOpacity(0.2), - width: 1, + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 384), + child: Card( + margin: const EdgeInsets.all(8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide( + color: colorScheme.outline.withOpacity(0.2), + width: 1, + ), ), - ), - color: colorScheme.surface, - elevation: 0.8, - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(16), - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CardTitle( - title: template.info.title, - icon: Icons.api, //currently no icons in the templates so icon always Icons.api - iconColor: colorScheme.primary, - ), - const SizedBox(height: 8), - CardDescription( - description: template.info.description.isEmpty ? 'No description' : template.info.description, - maxLines: 2, - ), - const SizedBox(height: 10), - Wrap( - spacing: 8, - runSpacing: 4, - children: template.info.tags.map((tag) => CustomChip.tag(tag, colorScheme)).toList(), - ), - ], + color: colorScheme.surface, + elevation: 0.8, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(16), + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: MediaQuery.of(context).size.width > 600 ? 16 : 12, + vertical: MediaQuery.of(context).size.width > 600 ? 16 : 12, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 3, + child: CardTitle( + title: template.info.title, + icon: Icons.api, // //currently no icons in the templates so icon always Icons.api + iconColor: colorScheme.primary, + ), + ), + Expanded( + flex: 5, + child: CardDescription( + description: template.info.description.isEmpty + ? 'No description' + : template.info.description, + maxLines: 5, + ), + ), + Expanded( + flex: 2, + child: Align( + alignment: Alignment.bottomLeft, + child: Wrap( + spacing: 8, + runSpacing: 4, + children: template.info.tags + .take(5) + .map((tag) => CustomChip.tag(tag, colorScheme)) + .toList(), + ), + ), + ), + ], + ), ), ), ), diff --git a/lib/screens/explorer/explorer_body.dart b/lib/screens/explorer/explorer_body.dart index 136fce570..ce1809b5b 100644 --- a/lib/screens/explorer/explorer_body.dart +++ b/lib/screens/explorer/explorer_body.dart @@ -26,6 +26,7 @@ class _ExplorerBodyState extends State { @override Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; return Container( color: Theme.of(context).colorScheme.background, child: Column( @@ -33,17 +34,19 @@ class _ExplorerBodyState extends State { Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), child: Center( - child: SizedBox( - width: MediaQuery.of(context).size.width * 0.6, - child: ApiSearchBar( - hintText: 'Search Explorer', - onChanged: (value) { - // TODO: Implement search filtering - // Example: Filter templates by title or tags - }, - onClear: () { - // TODO:Handle clear action - }, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.8, + child: ApiSearchBar( + hintText: 'Search Explorer', + onChanged: (value) { + // TODO: Implement search filtering + }, + onClear: () { + // TODO: Handle clear action + }, + ), ), ), ), @@ -63,9 +66,9 @@ class _ExplorerBodyState extends State { final templates = snapshot.data!; return GridView.builder( padding: const EdgeInsets.all(12), - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 384, - childAspectRatio: 1.6, + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 384, + childAspectRatio: 1.3, crossAxisSpacing: 12, mainAxisSpacing: 12, ), From fcaf3ad14d98e227232c6b72c2a8384b4fb6aa13 Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Mon, 28 Apr 2025 13:24:09 +0530 Subject: [PATCH 19/25] Add method parameter to RequestPane to display HTTP method chip on the left in RequestsCard Signed-off-by: Balasubramaniam12007 --- .../explorer/description/request_pane.dart | 2 ++ .../explorer/description/requests_card.dart | 27 ++++++++++++++----- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/lib/screens/explorer/description/request_pane.dart b/lib/screens/explorer/description/request_pane.dart index c2d0baebe..055232742 100644 --- a/lib/screens/explorer/description/request_pane.dart +++ b/lib/screens/explorer/description/request_pane.dart @@ -29,9 +29,11 @@ class _RequestsPaneState extends State { itemCount: widget.requests.length, itemBuilder: (context, index) { final request = widget.requests[index]; + final method = request.httpRequestModel?.method.toString().split('.').last.toUpperCase() ?? 'GET'; return RequestCard( title: request.name, isSelected: _selectedRequest == request, + method: method, onTap: () { setState(() { _selectedRequest = request; diff --git a/lib/screens/explorer/description/requests_card.dart b/lib/screens/explorer/description/requests_card.dart index 8f92a79af..2707ce95b 100644 --- a/lib/screens/explorer/description/requests_card.dart +++ b/lib/screens/explorer/description/requests_card.dart @@ -1,15 +1,18 @@ import 'package:flutter/material.dart'; +import '../common_widgets/chip.dart'; class RequestCard extends StatelessWidget { final String title; final bool isSelected; final VoidCallback? onTap; + final String method; const RequestCard({ super.key, required this.title, this.isSelected = false, this.onTap, + this.method = 'GET', }); @override @@ -29,12 +32,24 @@ class RequestCard extends StatelessWidget { width: double.infinity, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), - child: Text( - title, - style: TextStyle( - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.onSurface, - ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CustomChip.httpMethod(method), // HTTP method chip + const SizedBox(width: 12), + Expanded( + child: Text( + title, + style: TextStyle( + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface, + ), + textAlign: TextAlign.left, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ], ), ), ), From 70064ed46ae156971c36714e758835bf24304ac5 Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Tue, 29 Apr 2025 01:52:26 +0530 Subject: [PATCH 20/25] (doc) : add helper readme file --- lib/screens/explorer/doc/explorerREADME .MD | 143 ++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 lib/screens/explorer/doc/explorerREADME .MD diff --git a/lib/screens/explorer/doc/explorerREADME .MD b/lib/screens/explorer/doc/explorerREADME .MD new file mode 100644 index 000000000..2766d3a33 --- /dev/null +++ b/lib/screens/explorer/doc/explorerREADME .MD @@ -0,0 +1,143 @@ +# API Explorer Feature - APIDash (#PR 1) + +This temp README helps to monitor and understand the Proof of Concept (POC) for the **API Explorer** feature. + +The feature currently uses mock JSON data for templates (stored in `lib/screens/explorer/api_templates/mock`) and leverages reusable models from the `apidash_core` package to ensure consistency. It is built with Flutter, Riverpod for state management, and the `multi_split_view` package for a resizable split view layout. + +## Key Functionality + +### Current Implementation + +- **Template Browsing**: Displays API templates in a responsive grid, loaded from mock JSON files. +- **Template Details**: Provides a split view to show a list of requests and detailed information about the selected request. +- **Request Import**: Allows users to import a request into the main application with a single click. +- **Search (Placeholder)**: Includes a search bar for filtering templates (to be implemented). +- **Navigation**: Seamlessly integrates with the APIDash dashboard, accessible via the "Explorer" navigation option. + +## Key Files and Their Functionality + +### Models + +**Current Implementation**: + +The API Explorer reuses models from `apidash_core` and defines additional models specific to the feature. These models ensure consistency and enable serialization to/from JSON. + +1. `lib/models/explorer_model.dart` + + - **Purpose**: Defines the `ApiTemplate` and `Info` classes for representing API templates and their metadata. + - **Key Classes**: + - `ApiTemplate`: Contains an `Info` object (title, description, tags) and a list of `RequestModel`s. + - `Info`: Metadata for a template, serializable to/from JSON. + - **Functionality**: + - Parses JSON data from mock files into `ApiTemplate` objects. + - Supports future extensions (e.g., adding category or version fields to `Info`). + - **Reusability**: Designed to be extensible for real API data and reusable across other features. + +**Reusability Note**: The reuse of `RequestModel`, `HttpRequestModel`, and `HttpResponseModel` from `apidash_core` ensures consistency across APIDash features, reducing code duplication and simplifying maintenance. + +### Service + +1. `lib/services/templates_service.dart` + - **Purpose**: Loads API templates from mock JSON files or (in the future) a GitHub repo. + - **Key Methods**: + - `loadTemplates()`: Reads JSON files from `lib/screens/explorer/api_templates/mock` and parses them into `ApiTemplate` objects. + - `fetchTemplatesFromApi()`: Placeholder for fetching templates from a remote API. + - **Functionality**: + - Asynchronously loads templates using `rootBundle`. + - Handles errors gracefully, returning an empty list if loading fails. + - Filters JSON files from the asset manifest. + - **Future Potential**: + - Implement `fetchTemplatesFromrepo` for dynamic data. + - Add Hive to reduce redundant loads. + +### Important Widgets + +1. `lib/screens/explorer/explorer_page.dart` + + - **Purpose**: The main entry point for the API Explorer, managing navigation between the explorer grid and description page. + - **Functionality**: + - Toggles between the explorer view (`ExplorerHeader` + `ExplorerBody`) and the description view (`DescriptionPage`) using local state. + - Passes the selected template to `DescriptionPage` and handles back navigation. + - **Key Role**: Orchestrates the user flow within the API Explorer. + +2. `lib/screens/explorer/explorer_body.dart` + + - **Purpose**: Renders a searchable grid of API template cards. + - **Functionality**: + - Uses a `FutureBuilder` to load templates via `TemplatesService.loadTemplates()`. + - Displays loading, error, or empty states. + - Renders `TemplateCard` widgets in a responsive `GridView`. + - Includes an `ApiSearchBar` (placeholder for search filtering). + - **Key Role**: Provides the primary browsing interface. + +3. `lib/screens/explorer/description/description_page.dart` + + - **Purpose**: Displays detailed information about a selected API template. + - **Functionality**: + - Comprises a `DescriptionHeader` (title, description, back button) and a `DescriptionBody` (split view of requests and details). + - Receives the selected `ApiTemplate` and a callback to return to the explorer. + - **Key Role**: Facilitates in-depth exploration of templates. + +4. `lib/screens/explorer/description/description_body.dart` + + - **Purpose**: Manages the split view layout for the description page. + - **Functionality**: + - Uses `ExplorerSplitView` to create a resizable split view with `RequestsPane` (list of requests) and `DescriptionPane` (request details). + - Maintains state for the selected `RequestModel`. + - **Key Role**: Enables side-by-side viewing of requests and their details. + +5. `lib/screens/explorer/common_widgets/template_card.dart` + + - **Purpose**: Represents an API template in the explorer grid. + - **Functionality**: + - Displays the template's title (`CardTitle`), description (`CardDescription`), and tags (`CustomChip.tag`). + - Triggers navigation to the description page on tap. + - **Key Role**: Provides a visually appealing and informative template preview. + +6. `lib/screens/explorer/common_widgets/url_card.dart` + + - **Purpose**: Displays a request's URL, HTTP method, and an "Import" button. + - **Functionality**: + - Uses Riverpod to add the request to the collection via `collectionStateNotifierProvider`. + - Shows a `SnackBar` on successful import and navigates to the `HomePage`. + - **Key Role**: Enables seamless integration with the main application. + +7. `lib/screens/explorer/common_widgets/api_search_bar.dart` + + - **Purpose**: Provides a search input field for filtering templates. + - **Functionality**: + - Includes a search icon and a clear button (when text is entered). + - Currently a placeholder with TODOs for `onChanged` and `onClear` callbacks. + - **Key Role**: Foundation for future search functionality. + +8. `lib/screens/explorer/common_widgets/chip.dart` + + - **Purpose**: A versatile chip widget for HTTP methods, status codes, content types, and tags. + - **Functionality**: + - Provides factory constructors (e.g., `httpMethod`, `statusCode`, `tag`) with context-specific colors. + - Used across `TemplateCard`, `RequestCard`, and other widgets for consistent styling. + - **Key Role**: Enhances visual clarity and consistency. + +## Future Checklist + +The following tasks are planned to enhance the API Explorer feature: + +- [ ] **Implement Search Functionality**: + - Add filtering logic to `ApiSearchBar` to search templates by title, description, or tags. + - Update `ExplorerBody` to reflect filtered results dynamically. +- [ ] **Add Global State Management**: + - Introduce a Riverpod provider for managing selected template, selected request, and search query. + - Refactor `ExplorerPage` and `DescriptionBody` to use the provider. +- [ ] **Fetch Templates from GitHub Repo**: + - Implement `TemplatesService.fetchTemplatesFromRepo` to load templates from a remote endpoint. + - Add caching (e.g., using `hive`). +- [ ] **Enhance UI/UX**: + - Add sorting and filtering options to `ExplorerHeader` (e.g., by category or tag). + - Introduce template-specific icons in `TemplateCard`. + - Provide a placeholder UI in `DescriptionPane` when no request is selected. +- [ ] **Improve Performance**: + - Implement pagination or lazy loading in `ExplorerBody` for large template sets. + - Optimize `GridView` rendering for better scrolling performance. +- [ ] **Community Contributions** via API Dash: + - An in-app Documentation Editor will allow users to upload API specs, edit auto-generated JSON templates, download them, and (in the future) trigger GitHub pull requests directly . + \ No newline at end of file From 36c0c8cf3fc4e3667e808862057845710a149829 Mon Sep 17 00:00:00 2001 From: BALASUBRAMANIAM L Date: Tue, 29 Apr 2025 16:12:17 +0530 Subject: [PATCH 21/25] Create create-release.yml --- .../.github/workflows/create-release.yml | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/.github/workflows/create-release.yml diff --git a/.github/workflows/.github/workflows/create-release.yml b/.github/workflows/.github/workflows/create-release.yml new file mode 100644 index 000000000..e27efe261 --- /dev/null +++ b/.github/workflows/.github/workflows/create-release.yml @@ -0,0 +1,40 @@ +name: Release API Templates + +on: + push: + paths: + - 'lib/screens/explorer/api_templates/**' + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Zip API Templates + run: zip -r api_templates.zip lib/screens/explorer/api_templates + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: templates-${{ github.sha }} + release_name: API Templates Release ${{ github.sha }} + draft: false + prerelease: false + + - name: Upload Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./api_templates.zip + asset_name: api_templates.zip + asset_content_type: application/zip From c12484c36e22a5a9624ca97f69481964bbdf47bd Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Tue, 29 Apr 2025 16:15:11 +0530 Subject: [PATCH 22/25] (fix) dir bugs --- .github/workflows/{.github/workflows => }/create-release.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{.github/workflows => }/create-release.yml (100%) diff --git a/.github/workflows/.github/workflows/create-release.yml b/.github/workflows/create-release.yml similarity index 100% rename from .github/workflows/.github/workflows/create-release.yml rename to .github/workflows/create-release.yml From a21325f6cef5e61c57b3a266f5cdd8251b9f19a5 Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Tue, 29 Apr 2025 20:03:51 +0530 Subject: [PATCH 23/25] (feat:explorer) Add GitHub template fetching and Riverpod state management - Implemented TemplatesService to load mock templates and fetch from GitHub. - Added templatesProvider for reactive state management in ExplorerBody. - Created FetchTemplatesButton with SnackBar feedback for fetch status Signed-off-by: Balasubramaniam12007 --- lib/providers/templates_provider.dart | 64 ++++++++++++++ .../fetch_templates_button.dart | 48 +++++++++++ lib/screens/explorer/explorer_body.dart | 77 +++++++---------- lib/screens/explorer/explorer_header.dart | 4 +- lib/services/services.dart | 1 + lib/services/templates_service.dart | 86 +++++++++++++------ 6 files changed, 207 insertions(+), 73 deletions(-) create mode 100644 lib/providers/templates_provider.dart create mode 100644 lib/screens/explorer/common_widgets/fetch_templates_button.dart diff --git a/lib/providers/templates_provider.dart b/lib/providers/templates_provider.dart new file mode 100644 index 000000000..01df88bd9 --- /dev/null +++ b/lib/providers/templates_provider.dart @@ -0,0 +1,64 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/models/models.dart'; +import 'package:apidash/services/templates_service.dart'; + +class TemplatesState { + final List templates; + final bool isLoading; + final String? error; + + TemplatesState({ + this.templates = const [], + this.isLoading = false, + this.error, + }); + + TemplatesState copyWith({ + List? templates, + bool? isLoading, + String? error, + }) { + return TemplatesState( + templates: templates ?? this.templates, + isLoading: isLoading ?? this.isLoading, + error: error ?? this.error, + ); + } +} + +class TemplatesNotifier extends StateNotifier { + TemplatesNotifier() : super(TemplatesState()) { + loadMockTemplates(); + } + + Future loadMockTemplates() async { + state = state.copyWith(isLoading: true, error: null); + try { + final templates = await TemplatesService.loadTemplates(); + state = state.copyWith(templates: templates, isLoading: false); + } catch (e) { + state = state.copyWith( + templates: [], + isLoading: false, + error: 'Failed to load mock templates: $e', + ); + } + } + + Future fetchTemplatesFromGitHub() async { + state = state.copyWith(isLoading: true, error: null); + try { + final templates = await TemplatesService.fetchTemplatesFromGitHub(); + state = state.copyWith(templates: templates, isLoading: false); + } catch (e) { + state = state.copyWith( + isLoading: false, + error: 'Failed to fetch templates: $e', + ); + } + } +} + +final templatesProvider = StateNotifierProvider( + (ref) => TemplatesNotifier(), +); \ No newline at end of file diff --git a/lib/screens/explorer/common_widgets/fetch_templates_button.dart b/lib/screens/explorer/common_widgets/fetch_templates_button.dart new file mode 100644 index 000000000..6357efc21 --- /dev/null +++ b/lib/screens/explorer/common_widgets/fetch_templates_button.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/providers/templates_provider.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; + +class FetchTemplatesButton extends ConsumerWidget { + const FetchTemplatesButton({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final templatesState = ref.watch(templatesProvider); + + return ElevatedButton( + onPressed: templatesState.isLoading + ? null + : () async { + await ref.read(templatesProvider.notifier).fetchTemplatesFromGitHub(); + final newState = ref.read(templatesProvider); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + newState.error ?? 'Templates fetched successfully!', + style: const TextStyle(fontSize: 14), + ), + backgroundColor: newState.error != null + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.primary, + behavior: SnackBarBehavior.floating, + width: 400, + padding: kP12, + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of(context).colorScheme.onPrimary, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + child: templatesState.isLoading + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('Fetch Latest'), + ); + } +} \ No newline at end of file diff --git a/lib/screens/explorer/explorer_body.dart b/lib/screens/explorer/explorer_body.dart index ce1809b5b..fd2e02c20 100644 --- a/lib/screens/explorer/explorer_body.dart +++ b/lib/screens/explorer/explorer_body.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:apidash/services/services.dart'; -import 'common_widgets/common_widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/models/models.dart'; +import 'package:apidash/providers/templates_provider.dart'; +import 'package:apidash/screens/explorer/common_widgets/common_widgets.dart'; -class ExplorerBody extends StatefulWidget { +class ExplorerBody extends ConsumerWidget { final Function(ApiTemplate)? onCardTap; const ExplorerBody({ @@ -12,21 +13,9 @@ class ExplorerBody extends StatefulWidget { }); @override - _ExplorerBodyState createState() => _ExplorerBodyState(); -} + Widget build(BuildContext context, WidgetRef ref) { + final templatesState = ref.watch(templatesProvider); -class _ExplorerBodyState extends State { - late Future> _templatesFuture; - - @override - void initState() { - super.initState(); - _templatesFuture = TemplatesService.loadTemplates(); - } - - @override - Widget build(BuildContext context) { - final screenWidth = MediaQuery.of(context).size.width; return Container( color: Theme.of(context).colorScheme.background, child: Column( @@ -52,37 +41,29 @@ class _ExplorerBodyState extends State { ), ), Expanded( - child: FutureBuilder>( - future: _templatesFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } else if (snapshot.hasError) { - return Center(child: Text('Error: ${snapshot.error}')); - } else if (!snapshot.hasData || snapshot.data!.isEmpty) { - return const Center(child: Text('No templates found')); - } - - final templates = snapshot.data!; - return GridView.builder( - padding: const EdgeInsets.all(12), - gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 384, - childAspectRatio: 1.3, - crossAxisSpacing: 12, - mainAxisSpacing: 12, - ), - itemCount: templates.length, - itemBuilder: (context, index) { - final template = templates[index]; - return TemplateCard( - template: template, - onTap: () => widget.onCardTap?.call(template), - ); - }, - ); - }, - ), + child: templatesState.isLoading + ? const Center(child: CircularProgressIndicator()) + : templatesState.error != null + ? Center(child: Text('Error: ${templatesState.error}')) + : templatesState.templates.isEmpty + ? const Center(child: Text('No templates found')) + : GridView.builder( + padding: const EdgeInsets.all(12), + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 384, + childAspectRatio: 1.3, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + itemCount: templatesState.templates.length, + itemBuilder: (context, index) { + final template = templatesState.templates[index]; + return TemplateCard( + template: template, + onTap: () => onCardTap?.call(template), + ); + }, + ), ), ], ), diff --git a/lib/screens/explorer/explorer_header.dart b/lib/screens/explorer/explorer_header.dart index afdd98442..86099c9bc 100644 --- a/lib/screens/explorer/explorer_header.dart +++ b/lib/screens/explorer/explorer_header.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:apidash/screens/explorer/common_widgets/fetch_templates_button.dart'; class ExplorerHeader extends StatelessWidget { const ExplorerHeader({super.key}); @@ -14,7 +15,8 @@ class ExplorerHeader extends StatelessWidget { const Text( 'API EXPLORER', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ) + ), + const FetchTemplatesButton(), ], ), ); diff --git a/lib/services/services.dart b/lib/services/services.dart index 04f069005..a17b38dac 100644 --- a/lib/services/services.dart +++ b/lib/services/services.dart @@ -3,3 +3,4 @@ export 'history_service.dart'; export 'window_services.dart'; export 'shared_preferences_services.dart'; export 'templates_service.dart'; +export 'templates_service.dart'; \ No newline at end of file diff --git a/lib/services/templates_service.dart b/lib/services/templates_service.dart index 2dfa87fc7..840b9c31f 100644 --- a/lib/services/templates_service.dart +++ b/lib/services/templates_service.dart @@ -1,47 +1,85 @@ import 'dart:convert'; -import 'package:flutter/services.dart' show DefaultAssetBundle, rootBundle; +import 'package:flutter/services.dart' show rootBundle; +import 'package:http/http.dart' as http; +import 'package:archive/archive.dart'; import 'package:apidash/models/models.dart'; class TemplatesService { + static const String githubRepoOwner = 'BalaSubramaniam12007'; // Replace with your GitHub username + static const String githubRepoName = 'api-sample-library'; // Replace with your repository name static Future> loadTemplates() async { - - const String templatesDir = 'lib/screens/explorer/api_templates/mock'; // Directory containing JSON files - + const String templatesDir = 'lib/screens/explorer/api_templates/mock'; // Default mock templates directory try { final manifestContent = await rootBundle.loadString('AssetManifest.json'); final Map manifestMap = jsonDecode(manifestContent); - - // Filter for JSON files in the templates directory final jsonFiles = manifestMap.keys .where((key) => key.startsWith(templatesDir) && key.endsWith('.json')) .toList(); - List templates = []; for (String filePath in jsonFiles) { - try { - final String jsonString = await rootBundle.loadString(filePath); - final Map jsonData = jsonDecode(jsonString); + final String jsonString = await rootBundle.loadString(filePath); + final Map jsonData = jsonDecode(jsonString); + templates.add(ApiTemplate.fromJson(jsonData)); + } + return templates.isNotEmpty ? templates : _getFallbackTemplates(); + } catch (e) { + print('Error loading mock templates: $e'); + return _getFallbackTemplates(); + } + } + + static Future> fetchTemplatesFromGitHub() async { + try { + final releaseUrl = 'https://api.github.com/repos/$githubRepoOwner/$githubRepoName/releases/latest'; + final releaseResponse = await http.get( + Uri.parse(releaseUrl), + headers: {'Accept': 'application/vnd.github.v3+json'}, + ); + if (releaseResponse.statusCode != 200) { + throw Exception('Failed to fetch latest release: ${releaseResponse.statusCode}'); + } + + final releaseData = jsonDecode(releaseResponse.body); + final assets = releaseData['assets'] as List; + final zipAsset = assets.firstWhere( + (asset) => asset['name'] == 'api_templates.zip', + orElse: () => throw Exception('No api_templates.zip found in release'), + ); + + final zipUrl = zipAsset['browser_download_url'] as String; + final zipResponse = await http.get(Uri.parse(zipUrl)); + if (zipResponse.statusCode != 200) { + throw Exception('Failed to download zip: ${zipResponse.statusCode}'); + } + + final zipBytes = zipResponse.bodyBytes; + final archive = ZipDecoder().decodeBytes(zipBytes); + List templates = []; + for (final file in archive) { + if (file.isFile && file.name.endsWith('.json')) { + final jsonString = utf8.decode(file.content as List); + final jsonData = jsonDecode(jsonString); templates.add(ApiTemplate.fromJson(jsonData)); - } catch (e) { - print('Error loading $filePath: $e'); - // Future extensions: Log errors to a monitoring service. } } - - return templates; + return templates.isNotEmpty ? templates : _getFallbackTemplates(); } catch (e) { - print('Error loading templates: $e'); - return []; + print('Error fetching templates from GitHub: $e'); + throw Exception('Failed to fetch templates: $e'); } } - static Future> fetchTemplatesFromApi() async { - // Example implementation: - // final response = await http.get(Uri.parse('https://api.example.com/templates')); - // final List jsonList = jsonDecode(response.body); - // return jsonList.map((json) => ApiTemplate.fromJson(json)).toList(); - return []; + static List _getFallbackTemplates() { + return [ + ApiTemplate( + info: Info( + title: 'Default Template', + description: 'A fallback template when no templates are available.', + tags: ['default'], + ), + requests: [], + ), + ]; } - } \ No newline at end of file From 05748340f11e7119b810f0ceb9a24992d624aacd Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Tue, 29 Apr 2025 20:05:38 +0530 Subject: [PATCH 24/25] (feat:explorer) Integrate Hive for template caching with append strategy - Added kTemplatesBox in hive_service.dart to store templates separately. - Updated TemplatesService to append fetched templates to mocks in Hive. - Enhanced templatesProvider to track cached state for UI updates. Signed-off-by: Balasubramaniam12007 --- lib/providers/templates_provider.dart | 23 +++- .../fetch_templates_button.dart | 3 +- lib/services/hive_services.dart | 17 +++ lib/services/templates_service.dart | 113 ++++++++++++++---- 4 files changed, 127 insertions(+), 29 deletions(-) diff --git a/lib/providers/templates_provider.dart b/lib/providers/templates_provider.dart index 01df88bd9..a2d499729 100644 --- a/lib/providers/templates_provider.dart +++ b/lib/providers/templates_provider.dart @@ -6,41 +6,50 @@ class TemplatesState { final List templates; final bool isLoading; final String? error; + final bool isCached; TemplatesState({ this.templates = const [], this.isLoading = false, this.error, + this.isCached = false, }); TemplatesState copyWith({ List? templates, bool? isLoading, String? error, + bool? isCached, }) { return TemplatesState( templates: templates ?? this.templates, isLoading: isLoading ?? this.isLoading, error: error ?? this.error, + isCached: isCached ?? this.isCached, ); } } class TemplatesNotifier extends StateNotifier { TemplatesNotifier() : super(TemplatesState()) { - loadMockTemplates(); + loadInitialTemplates(); } - Future loadMockTemplates() async { + Future loadInitialTemplates() async { state = state.copyWith(isLoading: true, error: null); try { final templates = await TemplatesService.loadTemplates(); - state = state.copyWith(templates: templates, isLoading: false); + final isCached = await TemplatesService.hasCachedTemplates(); + state = state.copyWith( + templates: templates, + isLoading: false, + isCached: isCached, + ); } catch (e) { state = state.copyWith( templates: [], isLoading: false, - error: 'Failed to load mock templates: $e', + error: 'Failed to load templates: $e', ); } } @@ -49,7 +58,11 @@ class TemplatesNotifier extends StateNotifier { state = state.copyWith(isLoading: true, error: null); try { final templates = await TemplatesService.fetchTemplatesFromGitHub(); - state = state.copyWith(templates: templates, isLoading: false); + state = state.copyWith( + templates: templates, + isLoading: false, + isCached: true, + ); } catch (e) { state = state.copyWith( isLoading: false, diff --git a/lib/screens/explorer/common_widgets/fetch_templates_button.dart b/lib/screens/explorer/common_widgets/fetch_templates_button.dart index 6357efc21..05fceb337 100644 --- a/lib/screens/explorer/common_widgets/fetch_templates_button.dart +++ b/lib/screens/explorer/common_widgets/fetch_templates_button.dart @@ -19,7 +19,8 @@ class FetchTemplatesButton extends ConsumerWidget { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - newState.error ?? 'Templates fetched successfully!', + newState.error ?? + 'New templates fetched successfully!', style: const TextStyle(fontSize: 14), ), backgroundColor: newState.error != null diff --git a/lib/services/hive_services.dart b/lib/services/hive_services.dart index 5ca476073..3f9b5c38d 100644 --- a/lib/services/hive_services.dart +++ b/lib/services/hive_services.dart @@ -11,6 +11,9 @@ const String kHistoryMetaBox = "apidash-history-meta"; const String kHistoryBoxIds = "historyIds"; const String kHistoryLazyBox = "apidash-history-lazy"; +const String kTemplatesBox = "apidash-templates"; +const String kTemplatesKey = "templates"; + Future initHiveBoxes( bool initializeUsingPath, String? workspaceFolderPath, @@ -38,6 +41,7 @@ Future openHiveBoxes() async { await Hive.openBox(kEnvironmentBox); await Hive.openBox(kHistoryMetaBox); await Hive.openLazyBox(kHistoryLazyBox); + await Hive.openBox(kTemplatesBox); return true; } catch (e) { debugPrint("ERROR OPEN HIVE BOXES: $e"); @@ -59,6 +63,9 @@ Future clearHiveBoxes() async { if (Hive.isBoxOpen(kHistoryLazyBox)) { await Hive.lazyBox(kHistoryLazyBox).clear(); } + if (Hive.isBoxOpen(kTemplatesBox)) { + await Hive.box(kTemplatesBox).clear(); + } } catch (e) { debugPrint("ERROR CLEAR HIVE BOXES: $e"); } @@ -78,6 +85,9 @@ Future deleteHiveBoxes() async { if (Hive.isBoxOpen(kHistoryLazyBox)) { await Hive.lazyBox(kHistoryLazyBox).deleteFromDisk(); } + if (Hive.isBoxOpen(kTemplatesBox)) { + await Hive.box(kTemplatesBox).deleteFromDisk(); + } await Hive.close(); } catch (e) { debugPrint("ERROR DELETE HIVE BOXES: $e"); @@ -91,6 +101,7 @@ class HiveHandler { late final Box environmentBox; late final Box historyMetaBox; late final LazyBox historyLazyBox; + late final Box templatesBox; HiveHandler() { debugPrint("Trying to open Hive boxes"); @@ -98,6 +109,7 @@ class HiveHandler { environmentBox = Hive.box(kEnvironmentBox); historyMetaBox = Hive.box(kHistoryMetaBox); historyLazyBox = Hive.lazyBox(kHistoryLazyBox); + templatesBox = Hive.box(kTemplatesBox); } dynamic getIds() => dataBox.get(kKeyDataBoxIds); @@ -150,6 +162,7 @@ class HiveHandler { await environmentBox.clear(); await historyMetaBox.clear(); await historyLazyBox.clear(); + await templatesBox.clear(); } Future removeUnused() async { @@ -172,4 +185,8 @@ class HiveHandler { } } } + + dynamic getTemplates() => templatesBox.get(kTemplatesKey); + Future setTemplates(List>? templates) => + templatesBox.put(kTemplatesKey, templates); } diff --git a/lib/services/templates_service.dart b/lib/services/templates_service.dart index 840b9c31f..816abf0f2 100644 --- a/lib/services/templates_service.dart +++ b/lib/services/templates_service.dart @@ -3,30 +3,20 @@ import 'package:flutter/services.dart' show rootBundle; import 'package:http/http.dart' as http; import 'package:archive/archive.dart'; import 'package:apidash/models/models.dart'; +import 'package:apidash/services/services.dart'; class TemplatesService { - static const String githubRepoOwner = 'BalaSubramaniam12007'; // Replace with your GitHub username - static const String githubRepoName = 'api-sample-library'; // Replace with your repository name + static const String githubRepoOwner = 'BalaSubramaniam12007'; + static const String githubRepoName = 'api-sample-library'; static Future> loadTemplates() async { - const String templatesDir = 'lib/screens/explorer/api_templates/mock'; // Default mock templates directory - try { - final manifestContent = await rootBundle.loadString('AssetManifest.json'); - final Map manifestMap = jsonDecode(manifestContent); - final jsonFiles = manifestMap.keys - .where((key) => key.startsWith(templatesDir) && key.endsWith('.json')) - .toList(); - List templates = []; - for (String filePath in jsonFiles) { - final String jsonString = await rootBundle.loadString(filePath); - final Map jsonData = jsonDecode(jsonString); - templates.add(ApiTemplate.fromJson(jsonData)); - } - return templates.isNotEmpty ? templates : _getFallbackTemplates(); - } catch (e) { - print('Error loading mock templates: $e'); - return _getFallbackTemplates(); + // Load cached or default (mock) templates from Hive + final cachedTemplates = await loadCachedTemplates(); + if (cachedTemplates.isNotEmpty) { + return cachedTemplates; } + // Fallback to mock templates (initializes Hive with mocks) + return await _loadMockTemplates(); } static Future> fetchTemplatesFromGitHub() async { @@ -55,19 +45,96 @@ class TemplatesService { final zipBytes = zipResponse.bodyBytes; final archive = ZipDecoder().decodeBytes(zipBytes); - List templates = []; + List newTemplates = []; for (final file in archive) { if (file.isFile && file.name.endsWith('.json')) { final jsonString = utf8.decode(file.content as List); final jsonData = jsonDecode(jsonString); - templates.add(ApiTemplate.fromJson(jsonData)); + newTemplates.add(ApiTemplate.fromJson(jsonData)); } } - return templates.isNotEmpty ? templates : _getFallbackTemplates(); + + if (newTemplates.isNotEmpty) { + final existingTemplates = await loadCachedTemplates(); + final combinedTemplates = _appendTemplates(existingTemplates, newTemplates); + await _cacheTemplates(combinedTemplates); + return combinedTemplates; + } + return await loadCachedTemplates(); // Fallback to cached/mock } catch (e) { print('Error fetching templates from GitHub: $e'); - throw Exception('Failed to fetch templates: $e'); + // Fallback to cached or mock templates + return await loadCachedTemplates(); + } + } + + static Future> loadCachedTemplates() async { + try { + final templateJsons = hiveHandler.getTemplates(); + if (templateJsons != null && templateJsons is List) { + return templateJsons + .map((json) => ApiTemplate.fromJson(json as Map)) + .toList(); + } + // If no templates in Hive, initialize with mocks + return await _loadMockTemplates(); + } catch (e) { + print('Error loading cached templates: $e'); + return await _loadMockTemplates(); + } + } + + static Future hasCachedTemplates() async { + final templates = await loadCachedTemplates(); + return templates.isNotEmpty; + } + + static Future _cacheTemplates(List templates) async { + try { + final templateJsons = templates.map((t) => t.toJson()).toList(); + await hiveHandler.setTemplates(templateJsons); + } catch (e) { + print('Error caching templates: $e'); + } + } + + static Future> _loadMockTemplates() async { + const String templatesDir = 'lib/screens/explorer/api_templates/mock'; + try { + final manifestContent = await rootBundle.loadString('AssetManifest.json'); + final Map manifestMap = jsonDecode(manifestContent); + final jsonFiles = manifestMap.keys + .where((key) => key.startsWith(templatesDir) && key.endsWith('.json')) + .toList(); + List templates = []; + for (String filePath in jsonFiles) { + final String jsonString = await rootBundle.loadString(filePath); + final Map jsonData = jsonDecode(jsonString); + templates.add(ApiTemplate.fromJson(jsonData)); + } + + if (templates.isNotEmpty) { + await _cacheTemplates(templates); + return templates; + } + return _getFallbackTemplates(); + } catch (e) { + print('Error loading mock templates: $e'); + return _getFallbackTemplates(); + } + } + + static List _appendTemplates( + List existing, List newTemplates) { + final existingTitles = existing.map((t) => t.info.title.toLowerCase()).toSet(); + final combined = [...existing]; + for (final template in newTemplates) { + if (!existingTitles.contains(template.info.title.toLowerCase())) { + combined.add(template); + existingTitles.add(template.info.title.toLowerCase()); + } } + return combined; } static List _getFallbackTemplates() { From 961cc9af6e09d8a55d4fe5ea3ebbfd02f2480765 Mon Sep 17 00:00:00 2001 From: BALASUBRAMANIAM L Date: Wed, 30 Apr 2025 19:39:10 +0530 Subject: [PATCH 25/25] release test --- .../explorer/api_templates/webhook_api.json | 205 ++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 lib/screens/explorer/api_templates/webhook_api.json diff --git a/lib/screens/explorer/api_templates/webhook_api.json b/lib/screens/explorer/api_templates/webhook_api.json new file mode 100644 index 000000000..140305382 --- /dev/null +++ b/lib/screens/explorer/api_templates/webhook_api.json @@ -0,0 +1,205 @@ +{ + "info": { + "title": "Webhook Management API", + "description": "API for creating, configuring, and managing webhooks for event notifications", + "tags": ["webhooks", "events", "integrations", "notifications"] + }, + "requests": [ + { + "id": "webhook_register", + "apiType": "rest", + "name": "Register Webhook", + "description": "Register a new webhook endpoint to receive event notifications", + "httpRequestModel": { + "method": "post", + "url": "https://api.example.com/v1/webhooks", + "headers": [ + {"name": "Content-Type", "value": "application/json"}, + {"name": "Authorization", "value": "Bearer YOUR_API_KEY"} + ], + "params": [], + "isHeaderEnabledList": [true, true], + "isParamEnabledList": [], + "bodyContentType": "json", + "body": "{\n \"url\": \"https://your-server.example.com/webhook\",\n \"events\": [\"payment.created\", \"payment.succeeded\", \"payment.failed\"],\n \"description\": \"Payment notifications endpoint\",\n \"active\": true,\n \"secret\": \"whsec_8dj29dj29d8j29dj\"\n}", + "query": null, + "formData": null + }, + "responseStatus": 201, + "message": "Webhook registered successfully", + "httpResponseModel": { + "statusCode": 201, + "headers": { + "Content-Type": "application/json", + "Content-Length": "315", + "Location": "https://api.example.com/v1/webhooks/wh_92jd92j9d2j9" + }, + "requestHeaders": { + "Content-Type": "application/json", + "Authorization": "Bearer YOUR_API_KEY" + }, + "body": "{\n \"id\": \"wh_92jd92j9d2j9\",\n \"object\": \"webhook\",\n \"url\": \"https://your-server.example.com/webhook\",\n \"events\": [\"payment.created\", \"payment.succeeded\", \"payment.failed\"],\n \"description\": \"Payment notifications endpoint\",\n \"active\": true,\n \"created_at\": \"2025-04-29T10:05:32Z\"\n}", + "formattedBody": "{\n \"id\": \"wh_92jd92j9d2j9\",\n \"object\": \"webhook\",\n \"url\": \"https://your-server.example.com/webhook\",\n \"events\": [\"payment.created\", \"payment.succeeded\", \"payment.failed\"],\n \"description\": \"Payment notifications endpoint\",\n \"active\": true,\n \"created_at\": \"2025-04-29T10:05:32Z\"\n}", + "bodyBytes": null, + "time": 232000 + }, + "isWorking": false, + "sendingTime": null + }, + { + "id": "webhook_list", + "apiType": "rest", + "name": "List Webhooks", + "description": "Get all registered webhooks", + "httpRequestModel": { + "method": "get", + "url": "https://api.example.com/v1/webhooks", + "headers": [ + {"name": "Authorization", "value": "Bearer YOUR_API_KEY"} + ], + "params": [], + "isHeaderEnabledList": [true], + "isParamEnabledList": [], + "bodyContentType": "json", + "body": null, + "query": null, + "formData": null + }, + "responseStatus": 200, + "message": "Webhooks retrieved successfully", + "httpResponseModel": { + "statusCode": 200, + "headers": { + "Content-Type": "application/json", + "Content-Length": "562" + }, + "requestHeaders": { + "Authorization": "Bearer YOUR_API_KEY" + }, + "body": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"id\": \"wh_92jd92j9d2j9\",\n \"object\": \"webhook\",\n \"url\": \"https://your-server.example.com/webhook\",\n \"events\": [\"payment.created\", \"payment.succeeded\", \"payment.failed\"],\n \"description\": \"Payment notifications endpoint\",\n \"active\": true,\n \"created_at\": \"2025-04-29T10:05:32Z\"\n },\n {\n \"id\": \"wh_8jf83jf8jf38\",\n \"object\": \"webhook\",\n \"url\": \"https://your-app.example.org/hooks/refunds\",\n \"events\": [\"refund.created\", \"refund.updated\"],\n \"description\": \"Refund notifications\",\n \"active\": true,\n \"created_at\": \"2025-04-20T14:22:18Z\"\n }\n ],\n \"has_more\": false,\n \"total_count\": 2\n}", + "formattedBody": "{\n \"object\": \"list\",\n \"data\": [\n {\n \"id\": \"wh_92jd92j9d2j9\",\n \"object\": \"webhook\",\n \"url\": \"https://your-server.example.com/webhook\",\n \"events\": [\"payment.created\", \"payment.succeeded\", \"payment.failed\"],\n \"description\": \"Payment notifications endpoint\",\n \"active\": true,\n \"created_at\": \"2025-04-29T10:05:32Z\"\n },\n {\n \"id\": \"wh_8jf83jf8jf38\",\n \"object\": \"webhook\",\n \"url\": \"https://your-app.example.org/hooks/refunds\",\n \"events\": [\"refund.created\", \"refund.updated\"],\n \"description\": \"Refund notifications\",\n \"active\": true,\n \"created_at\": \"2025-04-20T14:22:18Z\"\n }\n ],\n \"has_more\": false,\n \"total_count\": 2\n}", + "bodyBytes": null, + "time": 180000 + }, + "isWorking": false, + "sendingTime": null + }, + { + "id": "webhook_update", + "apiType": "rest", + "name": "Update Webhook", + "description": "Update an existing webhook configuration", + "httpRequestModel": { + "method": "patch", + "url": "https://api.example.com/v1/webhooks/wh_92jd92j9d2j9", + "headers": [ + {"name": "Content-Type", "value": "application/json"}, + {"name": "Authorization", "value": "Bearer YOUR_API_KEY"} + ], + "params": [], + "isHeaderEnabledList": [true, true], + "isParamEnabledList": [], + "bodyContentType": "json", + "body": "{\n \"events\": [\"payment.created\", \"payment.succeeded\", \"payment.failed\", \"payment.refunded\"],\n \"description\": \"Updated payment notifications endpoint\"\n}", + "query": null, + "formData": null + }, + "responseStatus": 200, + "message": "Webhook updated successfully", + "httpResponseModel": { + "statusCode": 200, + "headers": { + "Content-Type": "application/json", + "Content-Length": "336" + }, + "requestHeaders": { + "Content-Type": "application/json", + "Authorization": "Bearer YOUR_API_KEY" + }, + "body": "{\n \"id\": \"wh_92jd92j9d2j9\",\n \"object\": \"webhook\",\n \"url\": \"https://your-server.example.com/webhook\",\n \"events\": [\"payment.created\", \"payment.succeeded\", \"payment.failed\", \"payment.refunded\"],\n \"description\": \"Updated payment notifications endpoint\",\n \"active\": true,\n \"created_at\": \"2025-04-29T10:05:32Z\",\n \"updated_at\": \"2025-04-29T10:20:15Z\"\n}", + "formattedBody": "{\n \"id\": \"wh_92jd92j9d2j9\",\n \"object\": \"webhook\",\n \"url\": \"https://your-server.example.com/webhook\",\n \"events\": [\"payment.created\", \"payment.succeeded\", \"payment.failed\", \"payment.refunded\"],\n \"description\": \"Updated payment notifications endpoint\",\n \"active\": true,\n \"created_at\": \"2025-04-29T10:05:32Z\",\n \"updated_at\": \"2025-04-29T10:20:15Z\"\n}", + "bodyBytes": null, + "time": 195000 + }, + "isWorking": false, + "sendingTime": null + }, + { + "id": "webhook_delete", + "apiType": "rest", + "name": "Delete Webhook", + "description": "Remove a webhook endpoint", + "httpRequestModel": { + "method": "delete", + "url": "https://api.example.com/v1/webhooks/wh_92jd92j9d2j9", + "headers": [ + {"name": "Authorization", "value": "Bearer YOUR_API_KEY"} + ], + "params": [], + "isHeaderEnabledList": [true], + "isParamEnabledList": [], + "bodyContentType": "json", + "body": null, + "query": null, + "formData": null + }, + "responseStatus": 204, + "message": "Webhook deleted successfully", + "httpResponseModel": { + "statusCode": 204, + "headers": { + "Content-Length": "0" + }, + "requestHeaders": { + "Authorization": "Bearer YOUR_API_KEY" + }, + "body": "", + "formattedBody": "", + "bodyBytes": null, + "time": 165000 + }, + "isWorking": false, + "sendingTime": null + }, + { + "id": "webhook_test", + "apiType": "rest", + "name": "Test Webhook", + "description": "Send a test event to a webhook endpoint", + "httpRequestModel": { + "method": "post", + "url": "https://api.example.com/v1/webhooks/wh_8jf83jf8jf38/test", + "headers": [ + {"name": "Content-Type", "value": "application/json"}, + {"name": "Authorization", "value": "Bearer YOUR_API_KEY"} + ], + "params": [], + "isHeaderEnabledList": [true, true], + "isParamEnabledList": [], + "bodyContentType": "json", + "body": "{\n \"event\": \"refund.created\"\n}", + "query": null, + "formData": null + }, + "responseStatus": 200, + "message": "Test event sent successfully", + "httpResponseModel": { + "statusCode": 200, + "headers": { + "Content-Type": "application/json", + "Content-Length": "254" + }, + "requestHeaders": { + "Content-Type": "application/json", + "Authorization": "Bearer YOUR_API_KEY" + }, + "body": "{\n \"id\": \"evt_test_83j93jd93j\",\n \"object\": \"event\",\n \"type\": \"refund.created\",\n \"created\": \"2025-04-29T10:35:42Z\",\n \"data\": {\n \"object\": {\n \"id\": \"ref_test_8j382j38\",\n \"object\": \"refund\",\n \"status\": \"succeeded\"\n }\n },\n \"pending_webhooks\": 1,\n \"request\": \"req_test_92j92j29d\"\n}", + "formattedBody": "{\n \"id\": \"evt_test_83j93jd93j\",\n \"object\": \"event\",\n \"type\": \"refund.created\",\n \"created\": \"2025-04-29T10:35:42Z\",\n \"data\": {\n \"object\": {\n \"id\": \"ref_test_8j382j38\",\n \"object\": \"refund\",\n \"status\": \"succeeded\"\n }\n },\n \"pending_webhooks\": 1,\n \"request\": \"req_test_92j92j29d\"\n}", + "bodyBytes": null, + "time": 215000 + }, + "isWorking": false, + "sendingTime": null + } + ] +} \ No newline at end of file