Merge pull request #101 from vidya-hub/form_data_imp

Form-Data and multi-part request feature added
This commit is contained in:
Ashita Prasad
2024-01-07 20:57:46 +05:30
committed by GitHub
25 changed files with 1114 additions and 78 deletions

View File

@ -1,8 +1,8 @@
import 'package:apidash/consts.dart';
import 'package:apidash/models/request_model.dart' show RequestModel;
import 'dart:convert';
import 'package:code_builder/code_builder.dart';
import 'package:dart_style/dart_style.dart';
import 'package:apidash/models/request_model.dart' show RequestModel;
import 'package:apidash/consts.dart';
import 'shared.dart';
class DartDioCodeGen {
@ -22,6 +22,7 @@ class DartDioCodeGen {
headers: requestModel.enabledHeadersMap,
body: requestModel.requestBody,
contentType: requestModel.requestBodyContentType,
formData: requestModel.formDataMapList,
);
return next;
} catch (e) {
@ -36,6 +37,7 @@ class DartDioCodeGen {
required Map<String, String> headers,
required String? body,
required ContentType contentType,
required List<Map<String, dynamic>> formData,
}) {
final sbf = StringBuffer();
final emitter = DartEmitter();
@ -54,9 +56,22 @@ class DartDioCodeGen {
literalMap(headers.map((key, value) => MapEntry(key, value))),
);
}
final multiPartList = Code('''
final List<Map<String,String>> formDataList = ${json.encode(formData)};
for (var formField in formDataList) {
if (formField['type'] == 'file') {
formData.files.add(MapEntry(
formField['name'],
await MultipartFile.fromFile(formField['value'], filename: formField['value']),
));
} else {
formData.fields.add(MapEntry(formField['name'], formField['value']));
}
}
''');
Expression? dataExp;
if (kMethodsWithBody.contains(method) && (body?.isNotEmpty ?? false)) {
if ((kMethodsWithBody.contains(method) && (body?.isNotEmpty ?? false) ||
contentType == ContentType.formdata)) {
final strContent = CodeExpression(Code('r\'\'\'$body\'\'\''));
switch (contentType) {
// dio dosen't need pass `content-type` header when body is json or plain text
@ -68,6 +83,8 @@ class DartDioCodeGen {
case ContentType.text:
dataExp = declareFinal('data').assign(strContent);
// when add new type of [ContentType], need update [dataExp].
case ContentType.formdata:
dataExp = declareFinal('data').assign(refer('FormData()'));
}
}
final responseExp = declareFinal('response').assign(InvokeExpression.newOf(
@ -93,6 +110,8 @@ class DartDioCodeGen {
if (queryParamExp != null) queryParamExp,
if (headerExp != null) headerExp,
if (dataExp != null) dataExp,
if ((contentType == ContentType.formdata && formData.isNotEmpty))
multiPartList,
responseExp,
refer('print').call([refer('response').property('statusCode')]),
refer('print').call([refer('response').property('data')]),
@ -122,6 +141,8 @@ class DartDioCodeGen {
sbf.writeln(mainFunction.accept(emitter));
return DartFormatter(pageWidth: 160).format(sbf.toString());
return DartFormatter(
pageWidth: contentType == ContentType.formdata ? 70 : 160)
.format(sbf.toString());
}
}

View File

@ -1,10 +1,9 @@
import 'dart:convert';
import 'dart:io';
import 'package:apidash/consts.dart';
import 'package:apidash/models/models.dart' show RequestModel;
import 'package:code_builder/code_builder.dart';
import 'package:dart_style/dart_style.dart';
import 'package:apidash/models/models.dart' show RequestModel;
import 'package:apidash/consts.dart';
import 'shared.dart';
class DartHttpCodeGen {
@ -22,9 +21,10 @@ class DartHttpCodeGen {
method: requestModel.method,
queryParams: requestModel.enabledParamsMap,
headers: {...requestModel.enabledHeadersMap},
body: requestModel.requestBody,
contentType: requestModel.requestBodyContentType,
hasContentTypeHeader: requestModel.hasContentTypeHeader,
hasContentTypeHeader: requestModel.hasContentTypeHeader,
body: requestModel.requestBody,
formData: requestModel.formDataMapList,
);
return next;
} catch (e) {
@ -37,9 +37,10 @@ class DartHttpCodeGen {
required HTTPVerb method,
required Map<String, String> queryParams,
required Map<String, String> headers,
required String? body,
required ContentType contentType,
required String? body,
required bool hasContentTypeHeader,
required List<Map<String, dynamic>> formData,
}) {
final uri = Uri.parse(url);
@ -55,7 +56,6 @@ class DartHttpCodeGen {
if (kMethodsWithBody.contains(method) && (body?.isNotEmpty ?? false)) {
final strContent = CodeExpression(Code('r\'\'\'$body\'\'\''));
dataExp = declareVar('body', type: refer('String')).assign(strContent);
if (!hasContentTypeHeader) {
headers.putIfAbsent(HttpHeaders.contentTypeHeader,
() => kContentTypeMap[contentType] ?? '');
@ -102,7 +102,9 @@ class DartHttpCodeGen {
);
}
final responseExp = declareFinal('response').assign(InvokeExpression.newOf(
refer('http.${method.name}'),
refer(
'http.${method.name}',
),
[refer('uri')],
{
if (headerExp != null) 'headers': refer('headers'),
@ -110,7 +112,36 @@ class DartHttpCodeGen {
},
[],
).awaited);
final multiPartRequest =
declareFinal('request').assign(InvokeExpression.newOf(
refer(
'http.MultipartRequest',
),
[refer(jsonEncode(method.name.toUpperCase())), refer('uri')],
));
final multiPartFiles = declareFinal('formDataList').assign(refer(
jsonEncode(formData),
));
final addHeaders = refer('request.headers.addAll').call([refer('headers')]);
const multiPartList = Code('''
for (Map<String, String> formData in formDataList){
if (formData['type'] == 'text') {
request.fields.addAll({formData['name']: formData['value']});
} else {
request.files.add(
await http.MultipartFile.fromPath(
formData['name'],
formData['value'],
),
);
}
}
''');
var multiPartRequestSend =
declareFinal('response').assign(refer('request.send()').awaited);
var multiPartResponseBody = declareFinal('responseBody')
.assign(refer('response.stream.bytesToString()').awaited);
final mainFunction = Method((m) {
final statusRef = refer('statusCode');
m
@ -135,25 +166,49 @@ class DartHttpCodeGen {
b.statements.add(headerExp.statement);
}
b.statements.add(const Code('\n'));
b.statements.add(responseExp.statement);
b.statements.add(const Code('\n'));
b.statements.add(declareVar('statusCode', type: refer('int'))
.assign(refer('response').property('statusCode'))
.statement);
if (contentType == ContentType.formdata) {
if (formData.isNotEmpty) {
b.statements.add(multiPartFiles.statement);
}
b.statements.add(multiPartRequest.statement);
if (formData.isNotEmpty) {
b.statements.add(multiPartList);
}
if (headerExp != null) {
b.statements.add(addHeaders.statement);
}
b.statements.add(multiPartRequestSend.statement);
b.statements.add(multiPartResponseBody.statement);
b.statements.add(declareVar('statusCode', type: refer('int'))
.assign(refer('response').property('statusCode'))
.statement);
b.statements.add(const Code('\n'));
} else {
b.statements.add(responseExp.statement);
b.statements.add(const Code('\n'));
b.statements.add(declareVar('statusCode', type: refer('int'))
.assign(refer('response').property('statusCode'))
.statement);
}
b.statements.add(declareIfElse(
condition: statusRef
.greaterOrEqualTo(literalNum(200))
.and(statusRef.lessThan(literalNum(300))),
body: [
refer('print').call([literalString(r'Status Code: $statusCode')]),
refer('print')
.call([literalString(r'Response Body: ${response.body}')]),
refer('print').call([
literalString(
'Response Body: ${contentType == ContentType.formdata ? ':\$responseBody' : '\${response.body}'}')
]),
],
elseBody: [
refer('print')
.call([literalString(r'Error Status Code: $statusCode')]),
refer('print').call(
[literalString(r'Error Response Body: ${response.body}')]),
refer('print').call([
literalString(
'Error Response Body: ${contentType == ContentType.formdata ? ':\$responseBody' : '\${response.body}'}')
]),
],
));
});
@ -161,6 +216,8 @@ class DartHttpCodeGen {
sbf.writeln(mainFunction.accept(emitter));
return DartFormatter(pageWidth: 160).format(sbf.toString());
return DartFormatter(
pageWidth: contentType == ContentType.formdata ? 70 : 160)
.format(sbf.toString());
}
}

View File

@ -1,16 +1,18 @@
import 'package:apidash/consts.dart';
import 'dart:convert';
import 'package:jinja/jinja.dart' as jj;
import 'package:apidash/utils/utils.dart'
show requestModelToHARJsonRequest, padMultilineString, stripUrlParams;
show padMultilineString, requestModelToHARJsonRequest, stripUrlParams;
import 'package:apidash/models/models.dart' show RequestModel;
import 'package:apidash/consts.dart';
class AxiosCodeGen {
AxiosCodeGen({this.isNodeJs = false});
final bool isNodeJs;
String kStringImportNode = """import axios from 'axios';
String kStringImportNode = """{% if isNodeJs %}import axios from 'axios';
{% endif %}{% if isFormDataRequest and isNodeJs %}const fs = require('fs');{% endif %}
""";
String kTemplateStart = """let config = {
@ -46,13 +48,49 @@ axios(config)
console.log(error);
});
""";
String kMultiPartBodyTemplate = r'''
async function buildFormData(fields) {
var formdata = new FormData();
for (const field of fields) {
const name = field.name || '';
const value = field.value || '';
const type = field.type || 'text';
if (type === 'text') {
formdata.append(name, value);
} else if (type === 'file') {
formdata.append(name,{% if isNodeJs %} fs.createReadStream(value){% else %} fileInput.files[0],value{% endif %});
}
}
return formdata;
}
''';
var kGetFormDataTemplate = '''buildFormData({{fields_list}});
''';
String? getCode(
RequestModel requestModel,
String defaultUriScheme,
) {
try {
String result = isNodeJs ? kStringImportNode : "";
jj.Template kNodejsImportTemplate = jj.Template(kStringImportNode);
String importsData = kNodejsImportTemplate.render({
"isFormDataRequest": requestModel.isFormDataRequest,
"isNodeJs": isNodeJs,
});
String result = importsData;
if (requestModel.isFormDataRequest &&
requestModel.formDataMapList.isNotEmpty) {
var templateMultiPartBody = jj.Template(kMultiPartBodyTemplate);
var renderedMultiPartBody = templateMultiPartBody.render({
"isNodeJs": isNodeJs,
});
result += renderedMultiPartBody;
}
String url = requestModel.url;
if (!url.contains("://") && url.isNotEmpty) {
@ -80,18 +118,31 @@ axios(config)
}
var headers = harJson["headers"];
if (headers.isNotEmpty) {
if (headers.isNotEmpty || requestModel.isFormDataRequest) {
var templateHeader = jj.Template(kTemplateHeader);
var m = {};
for (var i in headers) {
m[i["name"]] = i["value"];
}
if (requestModel.isFormDataRequest) {
m['Content-Type'] = 'multipart/form-data';
}
result += templateHeader
.render({"headers": padMultilineString(kEncoder.convert(m), 2)});
}
var templateBody = jj.Template(kTemplateBody);
if (requestModel.isFormDataRequest &&
requestModel.formDataMapList.isNotEmpty) {
var getFieldDataTemplate = jj.Template(kGetFormDataTemplate);
result += templateBody.render({
"body": getFieldDataTemplate.render({
"fields_list": json.encode(requestModel.formDataMapList),
})
});
}
if (harJson["postData"]?["text"] != null) {
var templateBody = jj.Template(kTemplateBody);
result += templateBody
.render({"body": kEncoder.convert(harJson["postData"]["text"])});
}

View File

@ -1,15 +1,18 @@
import 'package:apidash/consts.dart';
import 'dart:convert';
import 'package:jinja/jinja.dart' as jj;
import 'package:apidash/utils/utils.dart'
show requestModelToHARJsonRequest, padMultilineString;
show padMultilineString, requestModelToHARJsonRequest;
import 'package:apidash/models/models.dart' show RequestModel;
import 'package:apidash/consts.dart';
class FetchCodeGen {
FetchCodeGen({this.isNodeJs = false});
final bool isNodeJs;
String kStringImportNode = """import fetch from 'node-fetch';
String kStringImportNode = """
import fetch from 'node-fetch';
{% if isFormDataRequest %}const fs = require('fs');{% endif %}
""";
@ -28,6 +31,26 @@ let options = {
{{body}}
""";
String kMultiPartBodyTemplate = r'''
async function buildDataList(fields) {
var formdata = new FormData();
for (const field of fields) {
const name = field.name || '';
const value = field.value || '';
const type = field.type || 'text';
if (type === 'text') {
formdata.append(name, value);
} else if (type === 'file') {
formdata.append(name,{% if isNodeJs %} fs.createReadStream(value){% else %} fileInput.files[0],value{% endif %});
}
}
return formdata;
}
const payload = buildDataList({{fields_list}});
''';
String kStringRequest = """
};
@ -53,8 +76,19 @@ fetch(url, options)
String defaultUriScheme,
) {
try {
String result = isNodeJs ? kStringImportNode : "";
jj.Template kNodejsImportTemplate = jj.Template(kStringImportNode);
String importsData = kNodejsImportTemplate.render({
"isFormDataRequest": requestModel.isFormDataRequest,
});
String result = isNodeJs ? importsData : "";
if (requestModel.isFormDataRequest) {
var templateMultiPartBody = jj.Template(kMultiPartBodyTemplate);
result += templateMultiPartBody.render({
"isNodeJs": isNodeJs,
"fields_list": json.encode(requestModel.formDataMapList),
});
}
String url = requestModel.url;
if (!url.contains("://") && url.isNotEmpty) {
url = "$defaultUriScheme://$url";
@ -70,21 +104,33 @@ fetch(url, options)
});
var headers = harJson["headers"];
if (headers.isNotEmpty) {
var templateHeader = jj.Template(kTemplateHeader);
var m = {};
if (requestModel.isFormDataRequest) {
m["Content-Type"] = "multipart/form-data";
}
for (var i in headers) {
m[i["name"]] = i["value"];
}
result += templateHeader
.render({"headers": padMultilineString(kEncoder.convert(m), 2)});
result += templateHeader.render({
"headers": padMultilineString(kEncoder.convert(m), 2),
});
}
if (harJson["postData"]?["text"] != null) {
var templateBody = jj.Template(kTemplateBody);
result += templateBody
.render({"body": kEncoder.convert(harJson["postData"]["text"])});
result += templateBody.render({
"body": kEncoder.convert(harJson["postData"]["text"]),
});
} else if (requestModel.isFormDataRequest) {
var templateBody = jj.Template(kTemplateBody);
result += templateBody.render({
"body": 'payload',
});
}
result += kStringRequest;
return result;
} catch (e) {

View File

@ -1,9 +1,9 @@
import 'dart:convert';
import 'package:apidash/consts.dart';
import 'package:jinja/jinja.dart' as jj;
import 'package:apidash/utils/utils.dart'
show getValidRequestUri, stripUriParams;
import '../../models/request_model.dart';
import 'package:apidash/consts.dart';
class KotlinOkHttpCodeGen {
final String kTemplateStart = """import okhttp3.OkHttpClient
@ -61,6 +61,13 @@ import okhttp3.MediaType.Companion.toMediaType""";
}
""";
// Converting list of form data objects to kolin multi part data
String kFormDataBody = '''
val body = MultipartBody.Builder().setType(MultipartBody.FORM){% for item in formDataList %}{% if item.type == 'file' %}
.addFormDataPart("{{item.name}}",null,File("{{item.value}}").asRequestBody("application/octet-stream".toMediaType()))
{% else %}.addFormDataPart("{{item.name}}","{{item.value}}")
{% endif %}{% endfor %}.build()
''';
String? getCode(
RequestModel requestModel,
@ -68,7 +75,6 @@ import okhttp3.MediaType.Companion.toMediaType""";
) {
try {
String result = "";
bool hasHeaders = false;
bool hasQuery = false;
bool hasBody = false;
@ -102,7 +108,13 @@ import okhttp3.MediaType.Companion.toMediaType""";
var method = requestModel.method;
var requestBody = requestModel.requestBody;
if (kMethodsWithBody.contains(method) && requestBody != null) {
if (requestModel.isFormDataRequest) {
var formDataTemplate = jj.Template(kFormDataBody);
result += formDataTemplate.render({
"formDataList": requestModel.formDataMapList,
});
} else if (kMethodsWithBody.contains(method) && requestBody != null) {
var contentLength = utf8.encode(requestBody).length;
if (contentLength > 0) {
hasBody = true;
@ -127,7 +139,6 @@ import okhttp3.MediaType.Companion.toMediaType""";
if (headersList != null) {
var headers = requestModel.enabledHeadersMap;
if (headers.isNotEmpty) {
hasHeaders = true;
result += getHeaders(headers);
}
}
@ -135,7 +146,7 @@ import okhttp3.MediaType.Companion.toMediaType""";
var templateRequestEnd = jj.Template(kTemplateRequestEnd);
result += templateRequestEnd.render({
"method": method.name.toLowerCase(),
"hasBody": hasBody ? "body" : "",
"hasBody": (hasBody || requestModel.isFormDataRequest) ? "body" : "",
});
}
return result;

View File

@ -10,6 +10,9 @@ class cURLCodeGen {
String kTemplateHeader = """ \\
--header '{{name}}: {{value}}'
""";
String kTemplateFormData = """ \\
--form '{{name}}: {{value}}'
""";
String kTemplateBody = """ \\
--data '{{body}}'
@ -48,6 +51,23 @@ class cURLCodeGen {
.render({"name": item["name"], "value": item["value"]});
}
}
if (harJson['formData'] != null) {
var formDataList = harJson['formData'] as List<Map<String, dynamic>>;
for (var formData in formDataList) {
var templateFormData = jj.Template(kTemplateFormData);
if (formData['type'] != null &&
formData['name'] != null &&
formData['value'] != null &&
formData['name']!.isNotEmpty &&
formData['value']!.isNotEmpty) {
result += templateFormData.render({
"name": formData["name"],
"value":
"${formData['type'] == 'file' ? '@' : ''}${formData["value"]}",
});
}
}
}
if (harJson["postData"]?["text"] != null) {
var templateBody = jj.Template(kTemplateBody);

View File

@ -1,13 +1,16 @@
import 'dart:io';
import 'dart:convert';
import 'package:jinja/jinja.dart' as jj;
import 'package:apidash/consts.dart';
import 'package:apidash/utils/utils.dart'
show getValidRequestUri, padMultilineString;
show getNewUuid, getValidRequestUri, padMultilineString;
import 'package:apidash/models/models.dart' show RequestModel;
import 'package:apidash/consts.dart';
class PythonHttpClientCodeGen {
final String kTemplateStart = """import http.client
{% if isFormDataRequest %}import mimetypes
from codecs import encode
{% endif %}
""";
String kTemplateParams = """
@ -30,6 +33,8 @@ body = r'''{{body}}'''
headers = {{headers}}
""";
String kTemplateFormHeaderContentType = '''
multipart/form-data; boundary={{boundary}}''';
int kHeadersPadding = 10;
@ -55,10 +60,38 @@ data = res.read()
print(data.decode("utf-8"))
""";
final String kStringFormDataBody = r'''
def build_data_list(fields):
dataList = []
for field in fields:
name = field.get('name', '')
value = field.get('value', '')
type_ = field.get('type', 'text')
dataList.append(encode('--{{boundary}}'))
if type_ == 'text':
dataList.append(encode(f'Content-Disposition: form-data; name="{name}"'))
dataList.append(encode('Content-Type: text/plain'))
dataList.append(encode(''))
dataList.append(encode(value))
elif type_ == 'file':
dataList.append(encode(f'Content-Disposition: form-data; name="{name}"; filename="{value}"'))
dataList.append(encode(f'Content-Type: {mimetypes.guess_type(value)[0] or "application/octet-stream"}'))
dataList.append(encode(''))
dataList.append(open(value, 'rb').read())
dataList.append(encode(f'--{{boundary}}--'))
dataList.append(encode(''))
return dataList
dataList = build_data_list({{fields_list}})
body = b'\r\n'.join(dataList)
''';
String? getCode(
RequestModel requestModel,
String defaultUriScheme,
) {
String uuid = getNewUuid();
try {
String result = "";
bool hasHeaders = false;
@ -70,11 +103,17 @@ print(data.decode("utf-8"))
url = "$defaultUriScheme://$url";
}
result += kTemplateStart;
var templateStartUrl = jj.Template(kTemplateStart);
result += templateStartUrl.render(
{
"isFormDataRequest": requestModel.isFormDataRequest,
},
);
var rec = getValidRequestUri(
url,
requestModel.enabledRequestParams,
);
Uri? uri = rec.$1;
if (uri != null) {
@ -103,6 +142,14 @@ print(data.decode("utf-8"))
var headersList = requestModel.enabledRequestHeaders;
if (headersList != null || hasBody) {
var headers = requestModel.enabledHeadersMap;
if (requestModel.isFormDataRequest) {
var formHeaderTemplate =
jj.Template(kTemplateFormHeaderContentType);
headers[HttpHeaders.contentTypeHeader] = formHeaderTemplate.render({
"boundary": uuid,
});
}
if (headers.isNotEmpty || hasBody) {
hasHeaders = true;
if (hasBody && !requestModel.hasContentTypeHeader) {
@ -115,7 +162,15 @@ print(data.decode("utf-8"))
result += templateHeaders.render({"headers": headersString});
}
}
if (requestModel.isFormDataRequest) {
var formDataBodyData = jj.Template(kStringFormDataBody);
result += formDataBodyData.render(
{
"fields_list": json.encode(requestModel.formDataMapList),
"boundary": uuid,
},
);
}
var templateConnection = jj.Template(kTemplateConnection);
result += templateConnection.render({
"isHttps": uri.scheme == "https" ? "S" : "",
@ -129,11 +184,11 @@ print(data.decode("utf-8"))
"queryParamsStr": hasQuery ? " + queryParamsStr" : "",
});
if (hasBody) {
if (hasBody || requestModel.isFormDataRequest) {
result += kStringRequestBody;
}
if (hasHeaders) {
if (hasHeaders || requestModel.isFormDataRequest) {
result += kStringRequestHeaders;
}

View File

@ -3,12 +3,14 @@ import 'dart:convert';
import 'package:jinja/jinja.dart' as jj;
import 'package:apidash/consts.dart';
import 'package:apidash/utils/utils.dart'
show getValidRequestUri, padMultilineString, stripUriParams;
show getNewUuid, getValidRequestUri, padMultilineString, stripUriParams;
import 'package:apidash/models/models.dart' show RequestModel;
class PythonRequestsCodeGen {
final String kTemplateStart = """import requests
{% if isFormDataRequest %}import mimetypes
from codecs import encode
{% endif %}
url = '{{url}}'
""";
@ -37,6 +39,8 @@ payload = {{body}}
headers = {{headers}}
""";
String kTemplateFormHeaderContentType = '''
multipart/form-data; boundary={{boundary}}''';
int kHeadersPadding = 10;
@ -45,6 +49,34 @@ headers = {{headers}}
response = requests.{{method}}(url
""";
final String kStringFormDataBody = r'''
def build_data_list(fields):
dataList = []
for field in fields:
name = field.get('name', '')
value = field.get('value', '')
type_ = field.get('type', 'text')
dataList.append(encode('--{{boundary}}'))
if type_ == 'text':
dataList.append(encode(f'Content-Disposition: form-data; name="{name}"'))
dataList.append(encode('Content-Type: text/plain'))
dataList.append(encode(''))
dataList.append(encode(value))
elif type_ == 'file':
dataList.append(encode(f'Content-Disposition: form-data; name="{name}"; filename="{value}"'))
dataList.append(encode(f'Content-Type: {mimetypes.guess_type(value)[0] or "application/octet-stream"}'))
dataList.append(encode(''))
dataList.append(open(value, 'rb').read())
dataList.append(encode('--{{boundary}}--'))
dataList.append(encode(''))
return dataList
dataList = build_data_list({{fields_list}})
payload = b'\r\n'.join(dataList)
''';
String kStringRequestParams = """, params=params""";
String kStringRequestBody = """, data=payload""";
@ -69,6 +101,7 @@ print('Response Body:', response.text)
bool hasHeaders = false;
bool hasBody = false;
bool hasJsonBody = false;
String uuid = getNewUuid();
String url = requestModel.url;
if (!url.contains("://") && url.isNotEmpty) {
@ -82,7 +115,10 @@ print('Response Body:', response.text)
Uri? uri = rec.$1;
if (uri != null) {
var templateStartUrl = jj.Template(kTemplateStart);
result += templateStartUrl.render({"url": stripUriParams(uri)});
result += templateStartUrl.render({
"url": stripUriParams(uri),
'isFormDataRequest': requestModel.isFormDataRequest
});
if (uri.hasQuery) {
var params = uri.queryParameters;
@ -115,6 +151,13 @@ print('Response Body:', response.text)
var headersList = requestModel.enabledRequestHeaders;
if (headersList != null || hasBody) {
var headers = requestModel.enabledHeadersMap;
if (requestModel.isFormDataRequest) {
var formHeaderTemplate =
jj.Template(kTemplateFormHeaderContentType);
headers[HttpHeaders.contentTypeHeader] = formHeaderTemplate.render({
"boundary": uuid,
});
}
if (headers.isNotEmpty || hasBody) {
hasHeaders = true;
if (hasBody) {
@ -127,7 +170,15 @@ print('Response Body:', response.text)
result += templateHeaders.render({"headers": headersString});
}
}
if (requestModel.isFormDataRequest) {
var formDataBodyData = jj.Template(kStringFormDataBody);
result += formDataBodyData.render(
{
"fields_list": json.encode(requestModel.formDataMapList),
"boundary": uuid,
},
);
}
var templateRequest = jj.Template(kTemplateRequest);
result += templateRequest.render({
"method": method.name.toLowerCase(),
@ -137,15 +188,15 @@ print('Response Body:', response.text)
result += kStringRequestParams;
}
if (hasBody) {
if (hasBody || requestModel.isFormDataRequest) {
result += kStringRequestBody;
}
if (hasJsonBody) {
if (hasJsonBody || requestModel.isFormDataRequest) {
result += kStringRequestJson;
}
if (hasHeaders) {
if (hasHeaders || requestModel.isFormDataRequest) {
result += kStringRequestHeaders;
}

View File

@ -48,6 +48,10 @@ const kHintOpacity = 0.6;
const kForegroundOpacity = 0.05;
const kTextStyleButton = TextStyle(fontWeight: FontWeight.bold);
const kFormDataButtonLabelTextStyle = TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
);
const kBorderRadius8 = BorderRadius.all(Radius.circular(8));
final kBorderRadius10 = BorderRadius.circular(10);
@ -57,6 +61,7 @@ const kP1 = EdgeInsets.all(1);
const kP5 = EdgeInsets.all(5);
const kP8 = EdgeInsets.all(8);
const kPs8 = EdgeInsets.only(left: 8);
const kPs2 = EdgeInsets.only(left: 2);
const kPh20v5 = EdgeInsets.symmetric(horizontal: 20, vertical: 5);
const kPh20v10 = EdgeInsets.symmetric(horizontal: 20, vertical: 10);
const kP10 = EdgeInsets.all(10);
@ -85,7 +90,7 @@ const kP8CollectionPane = EdgeInsets.only(
// bottom: 8.0,
);
const kPr8CollectionPane = EdgeInsets.only(right: 8.0);
const kpsV5 = EdgeInsets.symmetric(vertical: 2);
const kHSpacer4 = SizedBox(width: 4);
const kHSpacer5 = SizedBox(width: 5);
const kHSpacer10 = SizedBox(width: 10);
@ -237,7 +242,9 @@ enum RequestItemMenuOption { edit, delete, duplicate }
enum HTTPVerb { get, head, post, put, patch, delete }
enum ContentType { json, text }
enum ContentType { json, text, formdata }
enum FormDataType { text, file }
const kSupportedUriSchemes = ["https", "http"];
const kDefaultUriScheme = "https";
@ -308,6 +315,7 @@ const kSubTypeDefaultViewOptions = 'all';
const kContentTypeMap = {
ContentType.json: "$kTypeApplication/$kSubTypeJson",
ContentType.text: "$kTypeText/$kSubTypePlain",
ContentType.formdata: "multipart/form-data",
};
enum ResponseBodyView { preview, code, raw, none }

View File

@ -0,0 +1,23 @@
import 'package:apidash/consts.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'form_data_model.freezed.dart';
part 'form_data_model.g.dart';
@freezed
class FormDataModel with _$FormDataModel {
const factory FormDataModel({
required String name,
required String value,
required FormDataType type,
}) = _FormDataModel;
factory FormDataModel.fromJson(Map<String, Object?> json) =>
_$FormDataModelFromJson(json);
}
const kFormDataEmptyModel = FormDataModel(
name: "",
value: "",
type: FormDataType.text,
);

View File

@ -0,0 +1,187 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'form_data_model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
FormDataModel _$FormDataModelFromJson(Map<String, dynamic> json) {
return _FormDataModel.fromJson(json);
}
/// @nodoc
mixin _$FormDataModel {
String get name => throw _privateConstructorUsedError;
String get value => throw _privateConstructorUsedError;
FormDataType get type => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$FormDataModelCopyWith<FormDataModel> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $FormDataModelCopyWith<$Res> {
factory $FormDataModelCopyWith(
FormDataModel value, $Res Function(FormDataModel) then) =
_$FormDataModelCopyWithImpl<$Res, FormDataModel>;
@useResult
$Res call({String name, String value, FormDataType type});
}
/// @nodoc
class _$FormDataModelCopyWithImpl<$Res, $Val extends FormDataModel>
implements $FormDataModelCopyWith<$Res> {
_$FormDataModelCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? name = null,
Object? value = null,
Object? type = null,
}) {
return _then(_value.copyWith(
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
value: null == value
? _value.value
: value // ignore: cast_nullable_to_non_nullable
as String,
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as FormDataType,
) as $Val);
}
}
/// @nodoc
abstract class _$$FormDataModelImplCopyWith<$Res>
implements $FormDataModelCopyWith<$Res> {
factory _$$FormDataModelImplCopyWith(
_$FormDataModelImpl value, $Res Function(_$FormDataModelImpl) then) =
__$$FormDataModelImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({String name, String value, FormDataType type});
}
/// @nodoc
class __$$FormDataModelImplCopyWithImpl<$Res>
extends _$FormDataModelCopyWithImpl<$Res, _$FormDataModelImpl>
implements _$$FormDataModelImplCopyWith<$Res> {
__$$FormDataModelImplCopyWithImpl(
_$FormDataModelImpl _value, $Res Function(_$FormDataModelImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? name = null,
Object? value = null,
Object? type = null,
}) {
return _then(_$FormDataModelImpl(
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
value: null == value
? _value.value
: value // ignore: cast_nullable_to_non_nullable
as String,
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as FormDataType,
));
}
}
/// @nodoc
@JsonSerializable()
class _$FormDataModelImpl implements _FormDataModel {
const _$FormDataModelImpl(
{required this.name, required this.value, required this.type});
factory _$FormDataModelImpl.fromJson(Map<String, dynamic> json) =>
_$$FormDataModelImplFromJson(json);
@override
final String name;
@override
final String value;
@override
final FormDataType type;
@override
String toString() {
return 'FormDataModel(name: $name, value: $value, type: $type)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$FormDataModelImpl &&
(identical(other.name, name) || other.name == name) &&
(identical(other.value, value) || other.value == value) &&
(identical(other.type, type) || other.type == type));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, name, value, type);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$FormDataModelImplCopyWith<_$FormDataModelImpl> get copyWith =>
__$$FormDataModelImplCopyWithImpl<_$FormDataModelImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$FormDataModelImplToJson(
this,
);
}
}
abstract class _FormDataModel implements FormDataModel {
const factory _FormDataModel(
{required final String name,
required final String value,
required final FormDataType type}) = _$FormDataModelImpl;
factory _FormDataModel.fromJson(Map<String, dynamic> json) =
_$FormDataModelImpl.fromJson;
@override
String get name;
@override
String get value;
@override
FormDataType get type;
@override
@JsonKey(ignore: true)
_$$FormDataModelImplCopyWith<_$FormDataModelImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,26 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'form_data_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$FormDataModelImpl _$$FormDataModelImplFromJson(Map<String, dynamic> json) =>
_$FormDataModelImpl(
name: json['name'] as String,
value: json['value'] as String,
type: $enumDecode(_$FormDataTypeEnumMap, json['type']),
);
Map<String, dynamic> _$$FormDataModelImplToJson(_$FormDataModelImpl instance) =>
<String, dynamic>{
'name': instance.name,
'value': instance.value,
'type': _$FormDataTypeEnumMap[instance.type]!,
};
const _$FormDataTypeEnumMap = {
FormDataType.text: 'text',
FormDataType.file: 'file',
};

View File

@ -2,3 +2,4 @@ export 'name_value_model.dart';
export 'request_model.dart';
export 'response_model.dart';
export 'settings_model.dart';
export 'form_data_model.dart';

View File

@ -1,10 +1,14 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:apidash/consts.dart';
import 'package:apidash/utils/utils.dart'
show mapToRows, rowsToMap, getEnabledRows;
import 'name_value_model.dart';
import 'response_model.dart';
import '../utils/utils.dart'
show
mapListToFormDataModelRows,
rowsToFormDataMapList,
mapToRows,
rowsToMap,
getEnabledRows;
import '../consts.dart';
import 'models.dart';
@immutable
class RequestModel {
@ -21,6 +25,7 @@ class RequestModel {
this.isParamEnabledList,
this.requestBodyContentType = ContentType.json,
this.requestBody,
this.requestFormDataList,
this.responseStatus,
this.message,
this.responseModel,
@ -38,6 +43,7 @@ class RequestModel {
final List<bool>? isParamEnabledList;
final ContentType requestBodyContentType;
final String? requestBody;
final List<FormDataModel>? requestFormDataList;
final int? responseStatus;
final String? message;
final ResponseModel? responseModel;
@ -54,6 +60,10 @@ class RequestModel {
Map<String, String> get headersMap => rowsToMap(requestHeaders) ?? {};
Map<String, String> get paramsMap => rowsToMap(requestParams) ?? {};
List<Map<String, dynamic>> get formDataMapList =>
rowsToFormDataMapList(requestFormDataList) ?? [];
bool get isFormDataRequest => requestBodyContentType == ContentType.formdata;
bool get hasContentTypeHeader => enabledHeadersMap.keys
.any((k) => k.toLowerCase() == HttpHeaders.contentTypeHeader);
@ -74,6 +84,8 @@ class RequestModel {
isParamEnabledList != null ? [...isParamEnabledList!] : null,
requestBodyContentType: requestBodyContentType,
requestBody: requestBody,
requestFormDataList:
requestFormDataList != null ? [...requestFormDataList!] : null,
);
}
@ -90,6 +102,7 @@ class RequestModel {
List<bool>? isParamEnabledList,
ContentType? requestBodyContentType,
String? requestBody,
List<FormDataModel>? requestFormDataList,
int? responseStatus,
String? message,
ResponseModel? responseModel,
@ -112,6 +125,7 @@ class RequestModel {
requestBodyContentType:
requestBodyContentType ?? this.requestBodyContentType,
requestBody: requestBody ?? this.requestBody,
requestFormDataList: requestFormDataList ?? this.requestFormDataList,
responseStatus: responseStatus ?? this.responseStatus,
message: message ?? this.message,
responseModel: responseModel ?? this.responseModel,
@ -143,9 +157,11 @@ class RequestModel {
requestBodyContentType = kDefaultContentType;
}
final requestBody = data["requestBody"] as String?;
final requestFormDataList = data["requestFormDataList"];
final responseStatus = data["responseStatus"] as int?;
final message = data["message"] as String?;
final responseModelJson = data["responseModel"];
if (responseModelJson != null) {
responseModel =
ResponseModel.fromJson(Map<String, dynamic>.from(responseModelJson));
@ -170,6 +186,9 @@ class RequestModel {
isParamEnabledList: isParamEnabledList,
requestBodyContentType: requestBodyContentType,
requestBody: requestBody,
requestFormDataList: requestFormDataList != null
? mapListToFormDataModelRows(List<Map>.from(requestFormDataList))
: null,
responseStatus: responseStatus,
message: message,
responseModel: responseModel,
@ -189,6 +208,7 @@ class RequestModel {
"isParamEnabledList": isParamEnabledList,
"requestBodyContentType": requestBodyContentType.name,
"requestBody": requestBody,
"requestFormDataList": rowsToFormDataMapList(requestFormDataList),
"responseStatus": includeResponse ? responseStatus : null,
"message": includeResponse ? message : null,
"responseModel": includeResponse ? responseModel?.toJson() : null,
@ -210,6 +230,7 @@ class RequestModel {
"Enabled Params: ${isParamEnabledList.toString()}",
"Request Body Content Type: ${requestBodyContentType.toString()}",
"Request Body: ${requestBody.toString()}",
"Request FormData: ${requestFormDataList.toString()}",
"Response Status: $responseStatus",
"Response Message: $message",
"Response: ${responseModel.toString()}"
@ -232,6 +253,7 @@ class RequestModel {
listEquals(other.isParamEnabledList, isParamEnabledList) &&
other.requestBodyContentType == requestBodyContentType &&
other.requestBody == requestBody &&
other.requestFormDataList == requestFormDataList &&
other.responseStatus == responseStatus &&
other.message == message &&
other.responseModel == responseModel;
@ -253,6 +275,7 @@ class RequestModel {
isParamEnabledList,
requestBodyContentType,
requestBody,
requestFormDataList,
responseStatus,
message,
responseModel,

View File

@ -5,6 +5,7 @@ import '../models/models.dart';
import '../services/services.dart' show hiveHandler, HiveHandler, request;
import '../utils/utils.dart' show uuid, collectionToHAR;
import '../consts.dart';
import 'package:http/http.dart' as http;
final activeIdStateProvider = StateProvider<String?>((ref) => null);
@ -127,6 +128,7 @@ class CollectionStateNotifier
List<bool>? isParamEnabledList,
ContentType? requestBodyContentType,
String? requestBody,
List<FormDataModel>? requestFormDataList,
int? responseStatus,
String? message,
ResponseModel? responseModel,
@ -143,9 +145,11 @@ class CollectionStateNotifier
isParamEnabledList: isParamEnabledList,
requestBodyContentType: requestBodyContentType,
requestBody: requestBody,
requestFormDataList: requestFormDataList,
responseStatus: responseStatus,
message: message,
responseModel: responseModel);
//print(newModel);
var map = {...state!};
map[id] = newModel;
state = map;
@ -156,10 +160,13 @@ class CollectionStateNotifier
ref.read(codePaneVisibleStateProvider.notifier).state = false;
final defaultUriScheme =
ref.read(settingsProvider.select((value) => value.defaultUriScheme));
RequestModel requestModel = state![id]!;
var responseRec =
await request(requestModel, defaultUriScheme: defaultUriScheme);
(http.Response?, Duration?, String?)? responseRec = await request(
requestModel,
defaultUriScheme: defaultUriScheme,
isMultiPartRequest:
requestModel.requestBodyContentType == ContentType.formdata,
);
late final RequestModel newRequestModel;
if (responseRec.$1 == null) {
newRequestModel = requestModel.copyWith(

View File

@ -18,6 +18,10 @@ class _EditRequestBodyState extends ConsumerState<EditRequestBody> {
final requestModel = ref
.read(collectionStateNotifierProvider.notifier)
.getRequestModel(activeId!);
ContentType? requestBodyStateWatcher = (ref
.watch(collectionStateNotifierProvider)![activeId]
?.requestBodyContentType) ??
ContentType.values.first;
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
@ -38,16 +42,18 @@ class _EditRequestBodyState extends ConsumerState<EditRequestBody> {
),
),
Expanded(
child: TextFieldEditor(
key: Key("$activeId-body"),
fieldKey: "$activeId-body-editor",
initialValue: requestModel?.requestBody,
onChanged: (String value) {
ref
.read(collectionStateNotifierProvider.notifier)
.update(activeId, requestBody: value);
},
),
child: requestBodyStateWatcher == ContentType.formdata
? const FormDataWidget()
: TextFieldEditor(
key: Key("$activeId-body"),
fieldKey: "$activeId-body-editor",
initialValue: requestModel?.requestBody,
onChanged: (String value) {
ref
.read(collectionStateNotifierProvider.notifier)
.update(activeId, requestBody: value);
},
),
)
],
),

View File

@ -9,6 +9,7 @@ import 'package:apidash/consts.dart';
Future<(http.Response?, Duration?, String?)> request(
RequestModel requestModel, {
String defaultUriScheme = kDefaultUriScheme,
bool isMultiPartRequest = false,
}) async {
(Uri?, String?) uriRec = getValidRequestUri(
requestModel.url,
@ -35,6 +36,31 @@ Future<(http.Response?, Duration?, String?)> request(
}
}
Stopwatch stopwatch = Stopwatch()..start();
if (isMultiPartRequest) {
var multiPartRequest = http.MultipartRequest(
requestModel.method.name.toUpperCase(),
requestUrl,
);
multiPartRequest.headers.addAll(headers);
for (FormDataModel formData
in (requestModel.requestFormDataList ?? [])) {
if (formData.type == FormDataType.text) {
multiPartRequest.fields.addAll({formData.name: formData.value});
} else {
multiPartRequest.files.add(
await http.MultipartFile.fromPath(
formData.name,
formData.value,
),
);
}
}
http.StreamedResponse multiPartResponse = await multiPartRequest.send();
stopwatch.stop();
http.Response convertedMultiPartResponse =
await convertStreamedResponse(multiPartResponse);
return (convertedMultiPartResponse, stopwatch.elapsed, null);
}
switch (requestModel.method) {
case HTTPVerb.get:
response = await http.get(requestUrl, headers: headers);

View File

@ -2,6 +2,7 @@ import 'dart:typed_data';
import 'dart:convert';
import '../models/models.dart';
import '../consts.dart';
import 'package:http/http.dart' as http;
String humanizeDuration(Duration? duration) {
if (duration == null) {
@ -89,6 +90,43 @@ List<NameValueModel>? mapToRows(Map<String, String>? kvMap) {
return finalRows;
}
List<Map<String, dynamic>>? rowsToFormDataMapList(
List<FormDataModel>? kvRows,
) {
if (kvRows == null) {
return null;
}
List<Map<String, dynamic>> finalMap = kvRows
.map((FormDataModel formData) => {
"name": formData.name,
"value": formData.value,
"type": formData.type.name,
})
.toList();
return finalMap;
}
List<FormDataModel>? mapListToFormDataModelRows(List<Map>? kvMap) {
if (kvMap == null) {
return null;
}
List<FormDataModel> finalRows = kvMap.map(
(formData) {
return FormDataModel(
name: formData["name"],
value: formData["value"],
type: getFormDataType(formData["type"]),
);
},
).toList();
return finalRows;
}
FormDataType getFormDataType(String? type) {
return FormDataType.values.firstWhere((element) => element.name == type,
orElse: () => FormDataType.text);
}
Uint8List? stringToBytes(String? text) {
if (text == null) {
return null;
@ -110,6 +148,23 @@ Uint8List jsonMapToBytes(Map<String, dynamic>? map) {
}
}
Future<http.Response> convertStreamedResponse(
http.StreamedResponse streamedResponse,
) async {
Uint8List bodyBytes = await streamedResponse.stream.toBytes();
http.Response response = http.Response.bytes(
bodyBytes,
streamedResponse.statusCode,
headers: streamedResponse.headers,
persistentConnection: streamedResponse.persistentConnection,
reasonPhrase: streamedResponse.reasonPhrase,
request: streamedResponse.request,
);
return response;
}
List<NameValueModel>? getEnabledRows(
List<NameValueModel>? rows, List<bool>? isRowEnabledList) {
if (rows == null || isRowEnabledList == null) {

View File

@ -1,6 +1,8 @@
import 'dart:convert';
import 'dart:io';
import 'package:apidash/consts.dart';
import 'package:apidash/models/form_data_model.dart';
import 'package:apidash/utils/convert_utils.dart';
import 'package:apidash/utils/utils.dart' show getValidRequestUri;
import 'package:apidash/models/models.dart' show RequestModel;
import 'package:package_info_plus/package_info_plus.dart';
@ -153,7 +155,9 @@ Map<String, dynamic> requestModelToHARJsonRequest(
}
}
}
if (requestModel.isFormDataRequest) {
json["formData"] = requestModel.formDataMapList;
}
if (exportMode) {
json["comment"] = "";
json["cookies"] = [];

View File

@ -93,6 +93,56 @@ class DropdownButtonContentType extends StatelessWidget {
}
}
class DropdownButtonFormData extends StatefulWidget {
const DropdownButtonFormData({
super.key,
this.formDataType,
this.onChanged,
});
final FormDataType? formDataType;
final void Function(FormDataType?)? onChanged;
@override
State<DropdownButtonFormData> createState() => _DropdownButtonFormData();
}
class _DropdownButtonFormData extends State<DropdownButtonFormData> {
@override
Widget build(BuildContext context) {
final surfaceColor = Theme.of(context).colorScheme.surface;
return DropdownButton<FormDataType>(
dropdownColor: surfaceColor,
focusColor: surfaceColor,
value: widget.formDataType,
icon: const Icon(
Icons.unfold_more_rounded,
size: 16,
),
elevation: 4,
style: kCodeStyle.copyWith(
color: Theme.of(context).colorScheme.primary,
),
underline: const IgnorePointer(),
onChanged: widget.onChanged,
borderRadius: kBorderRadius12,
items: FormDataType.values
.map<DropdownMenuItem<FormDataType>>((FormDataType value) {
return DropdownMenuItem<FormDataType>(
value: value,
child: Padding(
padding: kPs8,
child: Text(
value.name,
style: kTextStyleButton,
),
),
);
}).toList(),
);
}
}
class DropdownButtonCodegenLanguage extends StatelessWidget {
const DropdownButtonCodegenLanguage({
super.key,

View File

@ -0,0 +1,81 @@
import 'package:apidash/consts.dart';
import 'package:apidash/widgets/widgets.dart';
import 'package:flutter/material.dart';
class FormDataField extends StatefulWidget {
const FormDataField({
super.key,
required this.keyId,
this.initialValue,
this.hintText,
this.onChanged,
this.colorScheme,
this.formDataType,
this.onFormDataTypeChanged,
});
final String keyId;
final String? initialValue;
final String? hintText;
final void Function(String)? onChanged;
final ColorScheme? colorScheme;
final FormDataType? formDataType;
final void Function(FormDataType?)? onFormDataTypeChanged;
@override
State<FormDataField> createState() => _FormDataFieldState();
}
class _FormDataFieldState extends State<FormDataField> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
var colorScheme = widget.colorScheme ?? Theme.of(context).colorScheme;
return Row(
children: [
Expanded(
flex: 1,
child: TextFormField(
initialValue: widget.initialValue,
key: Key(widget.keyId),
style: kCodeStyle.copyWith(
color: colorScheme.onSurface,
),
decoration: InputDecoration(
hintStyle: kCodeStyle.copyWith(
color: colorScheme.outline.withOpacity(
kHintOpacity,
),
),
hintText: widget.hintText,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: colorScheme.primary.withOpacity(
kHintOpacity,
),
),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: colorScheme.surfaceVariant,
),
),
suffixIcon: DropdownButtonFormData(
formDataType: widget.formDataType,
onChanged: (p0) {
if (widget.onFormDataTypeChanged != null) {
widget.onFormDataTypeChanged!(p0);
}
},
)),
onChanged: widget.onChanged,
),
),
],
);
}
}

View File

@ -0,0 +1,223 @@
import 'dart:math';
import 'package:apidash/consts.dart';
import 'package:apidash/models/form_data_model.dart';
import 'package:apidash/models/models.dart';
import 'package:apidash/providers/collection_providers.dart';
import 'package:apidash/widgets/form_data_field.dart';
import 'package:apidash/widgets/textfields.dart';
import 'package:davi/davi.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class FormDataWidget extends ConsumerStatefulWidget {
const FormDataWidget({super.key});
@override
ConsumerState<FormDataWidget> createState() => _FormDataBodyState();
}
class _FormDataBodyState extends ConsumerState<FormDataWidget> {
late int seed;
final random = Random.secure();
late List<FormDataModel> rows;
@override
void initState() {
super.initState();
seed = random.nextInt(kRandMax);
}
@override
Widget build(BuildContext context) {
final activeId = ref.watch(activeIdStateProvider);
var formRows = ref.read(activeRequestModelProvider)?.requestFormDataList;
rows =
formRows == null || formRows.isEmpty ? [kFormDataEmptyModel] : formRows;
DaviModel<FormDataModel> daviModelRows = DaviModel<FormDataModel>(
rows: rows,
columns: [
DaviColumn(
cellPadding: kpsV5,
name: 'Key',
grow: 4,
cellBuilder: (_, row) {
int idx = row.index;
return Theme(
data: Theme.of(context),
child: FormDataField(
keyId: "$activeId-$idx-form-v-$seed",
initialValue: rows[idx].name,
hintText: " Add Key",
onChanged: (value) {
rows[idx] = rows[idx].copyWith(
name: value,
);
_onFieldChange(activeId!);
},
colorScheme: Theme.of(context).colorScheme,
formDataType: rows[idx].type,
onFormDataTypeChanged: (value) {
rows[idx] = rows[idx].copyWith(
type: value ?? FormDataType.text,
);
rows[idx] = rows[idx].copyWith(value: "");
setState(() {});
_onFieldChange(activeId!);
},
),
);
},
sortable: false,
),
DaviColumn(
width: 40,
cellPadding: kpsV5,
cellAlignment: Alignment.center,
cellBuilder: (_, row) {
return Text(
"=",
style: kCodeStyle,
);
},
),
DaviColumn(
name: 'Value',
grow: 4,
cellPadding: kpsV5,
cellBuilder: (_, row) {
int idx = row.index;
return rows[idx].type == FormDataType.file
? Align(
alignment: Alignment.centerLeft,
child: Row(
children: [
Expanded(
child: Theme(
data: Theme.of(context),
child: ElevatedButton.icon(
icon: const Icon(
Icons.snippet_folder_rounded,
size: 20,
),
style: ButtonStyle(
shape: MaterialStatePropertyAll(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
),
),
onPressed: () async {
FilePickerResult? pickedResult =
await FilePicker.platform.pickFiles();
if (pickedResult != null &&
pickedResult.files.isNotEmpty &&
pickedResult.files.first.path != null) {
rows[idx] = rows[idx].copyWith(
value: pickedResult.files.first.path!,
);
setState(() {});
_onFieldChange(activeId!);
}
},
label: Text(
(rows[idx].type == FormDataType.file &&
rows[idx].value.isNotEmpty)
? rows[idx].value.toString()
: "Select File",
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
style: kFormDataButtonLabelTextStyle,
),
),
),
),
],
),
)
: CellField(
keyId: "$activeId-$idx-form-v-$seed",
initialValue: rows[idx].value,
hintText: " Add Value",
onChanged: (value) {
rows[idx] = rows[idx].copyWith(value: value);
_onFieldChange(activeId!);
},
colorScheme: Theme.of(context).colorScheme,
);
},
sortable: false,
),
DaviColumn(
pinStatus: PinStatus.none,
width: 30,
cellBuilder: (_, row) {
return InkWell(
child: Theme.of(context).brightness == Brightness.dark
? kIconRemoveDark
: kIconRemoveLight,
onTap: () {
seed = random.nextInt(kRandMax);
if (rows.length == 1) {
setState(() {
rows = [kFormDataEmptyModel];
});
} else {
rows.removeAt(row.index);
}
_onFieldChange(activeId!);
setState(() {});
},
);
},
),
],
);
return Stack(
children: [
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: kBorderRadius12,
),
margin: kP10,
child: Column(
children: [
Expanded(
child: DaviTheme(
data: kTableThemeData,
child: Davi<FormDataModel>(daviModelRows),
),
),
],
),
),
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.only(bottom: 30),
child: ElevatedButton.icon(
onPressed: () {
setState(() {
rows.add(kFormDataEmptyModel);
});
_onFieldChange(activeId!);
},
icon: const Icon(Icons.add),
label: const Text(
"Add Form Data",
style: kTextStyleButton,
),
),
),
),
],
);
}
void _onFieldChange(String activeId) {
ref.read(collectionStateNotifierProvider.notifier).update(
activeId,
requestFormDataList: rows,
);
}
}

View File

@ -22,3 +22,4 @@ export 'textfields.dart';
export 'texts.dart';
export 'uint8_audio_player.dart';
export 'window_caption.dart';
export 'form_data_widget.dart';

View File

@ -48,6 +48,7 @@ dependencies:
url: https://github.com/foss42/json_data_explorer.git
version: ^0.1.1
scrollable_positioned_list: ^0.2.3
file_picker: ^6.1.1
flutter_svg: ^2.0.9
vector_graphics_compiler: ^1.1.9+1
code_builder: ^4.9.0

View File

@ -113,6 +113,7 @@ void main() {
"requestBody": '''{
"text":"WORLD"
}''',
'requestFormDataList': null,
'responseStatus': null,
'message': null,
'responseModel': null
@ -147,6 +148,7 @@ void main() {
"Enabled Params: null",
"Request Body Content Type: ContentType.json",
'Request Body: {\n"text":"WORLD"\n}',
'Request FormData: null',
"Response Status: null",
"Response Message: null",
"Response: null"