mirror of
https://github.com/foss42/apidash.git
synced 2025-05-30 05:21:15 +08:00
feat: Multi Part Request Feature Added
This commit is contained in:
@ -301,12 +301,12 @@ const kContentTypeMap = {
|
||||
ContentType.formdata: "multipart/form-data",
|
||||
};
|
||||
const kFormDataTypeMap = {
|
||||
FormDataType.file: "File",
|
||||
FormDataType.text: "Text",
|
||||
FormDataType.file: "file",
|
||||
FormDataType.text: "text",
|
||||
};
|
||||
const kMapFormDataType = {
|
||||
"File": FormDataType.file,
|
||||
"Text": FormDataType.text,
|
||||
"file": FormDataType.file,
|
||||
"text": FormDataType.text,
|
||||
};
|
||||
|
||||
enum ResponseBodyView { preview, code, raw, none }
|
||||
|
@ -1,5 +1,7 @@
|
||||
import 'package:apidash/models/form_data_model.dart';
|
||||
import 'package:apidash/services/http_service.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../consts.dart';
|
||||
import '../models/models.dart';
|
||||
@ -158,10 +160,20 @@ class CollectionStateNotifier
|
||||
ref.read(codePaneVisibleStateProvider.notifier).state = false;
|
||||
final defaultUriScheme =
|
||||
ref.read(settingsProvider.select((value) => value.defaultUriScheme));
|
||||
|
||||
(http.Response?, Duration?, String?)? responseRec;
|
||||
RequestModel requestModel = state![id]!;
|
||||
var responseRec =
|
||||
await request(requestModel, defaultUriScheme: defaultUriScheme);
|
||||
if (requestModel.formDataList != null &&
|
||||
requestModel.formDataList!.isNotEmpty) {
|
||||
responseRec = await multiPartRequest(
|
||||
requestModel,
|
||||
defaultUriScheme: defaultUriScheme,
|
||||
);
|
||||
} else {
|
||||
responseRec = await request(
|
||||
requestModel,
|
||||
defaultUriScheme: defaultUriScheme,
|
||||
);
|
||||
}
|
||||
late final RequestModel newRequestModel;
|
||||
if (responseRec.$1 == null) {
|
||||
newRequestModel = requestModel.copyWith(
|
||||
@ -180,7 +192,6 @@ class CollectionStateNotifier
|
||||
responseModel: responseModel,
|
||||
);
|
||||
}
|
||||
//print(newRequestModel);
|
||||
ref.read(sentRequestIdStateProvider.notifier).state = null;
|
||||
var map = {...state!};
|
||||
map[id] = newRequestModel;
|
||||
|
@ -1,13 +1,9 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:apidash/consts.dart';
|
||||
import 'package:apidash/models/form_data_model.dart';
|
||||
import 'package:apidash/providers/providers.dart';
|
||||
import 'package:apidash/utils/extensions/file_extension.dart';
|
||||
import 'package:apidash/widgets/form_data_field.dart';
|
||||
import 'package:apidash/widgets/form_data_widget.dart';
|
||||
import 'package:apidash/widgets/widgets.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';
|
||||
|
||||
@ -19,15 +15,12 @@ class EditRequestBody extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _EditRequestBodyState extends ConsumerState<EditRequestBody> {
|
||||
List<FormDataModel> rows = [];
|
||||
final random = Random.secure();
|
||||
late int seed;
|
||||
late FilePicker filePicker;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
seed = random.nextInt(kRandMax);
|
||||
filePicker = FilePicker.platform;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -40,138 +33,6 @@ class _EditRequestBodyState extends ConsumerState<EditRequestBody> {
|
||||
.watch(collectionStateNotifierProvider)![activeId]
|
||||
?.requestBodyContentType) ??
|
||||
ContentType.values.first;
|
||||
DaviModel<FormDataModel> model = DaviModel<FormDataModel>(
|
||||
rows: rows,
|
||||
columns: [
|
||||
DaviColumn(
|
||||
name: 'Key',
|
||||
grow: 1,
|
||||
cellBuilder: (_, row) {
|
||||
int idx = row.index;
|
||||
return SizedBox(
|
||||
child: FormDataField(
|
||||
keyId: "$activeId-$idx-form-v-$seed",
|
||||
initialValue: rows[idx].value,
|
||||
hintText: " 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: "");
|
||||
_onFieldChange(activeId);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
sortable: false,
|
||||
),
|
||||
DaviColumn(
|
||||
width: 10,
|
||||
cellBuilder: (_, row) {
|
||||
return Text(
|
||||
"=",
|
||||
style: kCodeStyle,
|
||||
);
|
||||
},
|
||||
),
|
||||
DaviColumn(
|
||||
name: 'Value',
|
||||
grow: 4,
|
||||
cellBuilder: (_, row) {
|
||||
int idx = row.index;
|
||||
return rows[idx].type == FormDataType.file
|
||||
? Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: kPs2,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
FilePickerResult? pickedResult =
|
||||
await filePicker.pickFiles();
|
||||
if (pickedResult != null &&
|
||||
pickedResult.files.isNotEmpty) {
|
||||
rows[idx] = rows[idx].copyWith(
|
||||
value: pickedResult.files.first.path,
|
||||
);
|
||||
_onFieldChange(activeId);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
"Select File",
|
||||
style: kTextStyleButton.copyWith(
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: kPs2,
|
||||
child: Text(
|
||||
rows[idx].type == FormDataType.file
|
||||
? (rows[idx].value != null
|
||||
? rows[idx].value.toString().fileName
|
||||
: "")
|
||||
: "",
|
||||
style: kTextStyleButton,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: CellField(
|
||||
keyId: "$activeId-$idx-form-v-$seed",
|
||||
initialValue: rows[idx].value,
|
||||
hintText: " 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);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
@ -193,43 +54,11 @@ class _EditRequestBodyState extends ConsumerState<EditRequestBody> {
|
||||
),
|
||||
Expanded(
|
||||
child: requestBodyStateWatcher == ContentType.formdata
|
||||
? 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>(model),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 30),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
rows.add(kFormDataEmptyModel);
|
||||
_onFieldChange(activeId);
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text(
|
||||
"Add Form Data",
|
||||
style: kTextStyleButton,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
? FormDataWidget(
|
||||
seed: seed,
|
||||
onFormDataRemove: () {
|
||||
seed = random.nextInt(kRandMax);
|
||||
},
|
||||
)
|
||||
: TextFieldEditor(
|
||||
key: Key("$activeId-body"),
|
||||
@ -246,13 +75,6 @@ class _EditRequestBodyState extends ConsumerState<EditRequestBody> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onFieldChange(String activeId) {
|
||||
ref.read(collectionStateNotifierProvider.notifier).update(
|
||||
activeId,
|
||||
formDataList: rows,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DropdownButtonBodyContentType extends ConsumerStatefulWidget {
|
||||
|
@ -1,10 +1,12 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:apidash/utils/utils.dart';
|
||||
import 'package:apidash/models/models.dart';
|
||||
|
||||
import 'package:apidash/consts.dart';
|
||||
import 'package:apidash/models/form_data_model.dart';
|
||||
import 'package:apidash/models/models.dart';
|
||||
import 'package:apidash/utils/utils.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
Future<(http.Response?, Duration?, String?)> request(
|
||||
RequestModel requestModel, {
|
||||
@ -63,3 +65,59 @@ Future<(http.Response?, Duration?, String?)> request(
|
||||
return (null, null, uriRec.$2);
|
||||
}
|
||||
}
|
||||
|
||||
Future<(http.Response?, Duration?, String?)> multiPartRequest(
|
||||
RequestModel requestModel, {
|
||||
String defaultUriScheme = kDefaultUriScheme,
|
||||
}) async {
|
||||
(Uri?, String?) uriRec = getValidRequestUri(
|
||||
requestModel.url,
|
||||
requestModel.requestParams,
|
||||
defaultUriScheme: defaultUriScheme,
|
||||
);
|
||||
if (uriRec.$1 != null) {
|
||||
Uri requestUrl = uriRec.$1!;
|
||||
Map<String, String> headers = requestModel.headersMap;
|
||||
try {
|
||||
var requestBody = requestModel.requestBody;
|
||||
if (kMethodsWithBody.contains(requestModel.method) &&
|
||||
requestBody != null) {
|
||||
var contentLength = utf8.encode(requestBody).length;
|
||||
if (contentLength > 0) {
|
||||
headers[HttpHeaders.contentLengthHeader] = contentLength.toString();
|
||||
headers[HttpHeaders.contentTypeHeader] =
|
||||
kContentTypeMap[requestModel.requestBodyContentType] ?? "";
|
||||
}
|
||||
}
|
||||
Stopwatch stopwatch = Stopwatch()..start();
|
||||
|
||||
var request = http.MultipartRequest(
|
||||
requestModel.method.name.toUpperCase(),
|
||||
requestUrl,
|
||||
);
|
||||
for (FormDataModel formData in (requestModel.formDataList ?? [])) {
|
||||
if (formData.type == FormDataType.text) {
|
||||
request.fields.addAll({formData.name: formData.value});
|
||||
} else {
|
||||
request.files.add(
|
||||
await http.MultipartFile.fromPath(
|
||||
formData.name,
|
||||
formData.value,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
http.StreamedResponse response = await request.send();
|
||||
|
||||
stopwatch.stop();
|
||||
http.Response convertedHttpResponse =
|
||||
await convertStreamedResponse(response);
|
||||
return (convertedHttpResponse, stopwatch.elapsed, null);
|
||||
} catch (e) {
|
||||
return (null, null, e.toString());
|
||||
}
|
||||
} else {
|
||||
return (null, null, uriRec.$2);
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:apidash/models/form_data_model.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../consts.dart';
|
||||
import '../models/models.dart';
|
||||
@ -142,3 +143,20 @@ Uint8List jsonMapToBytes(Map<String, dynamic>? map) {
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
212
lib/widgets/form_data_widget.dart
Normal file
212
lib/widgets/form_data_widget.dart
Normal file
@ -0,0 +1,212 @@
|
||||
import 'package:apidash/consts.dart';
|
||||
import 'package:apidash/models/form_data_model.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,
|
||||
required this.seed,
|
||||
required this.onFormDataRemove,
|
||||
});
|
||||
final int seed;
|
||||
final Function onFormDataRemove;
|
||||
@override
|
||||
ConsumerState<FormDataWidget> createState() => _FormDataBodyState();
|
||||
}
|
||||
|
||||
class _FormDataBodyState extends ConsumerState<FormDataWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final activeId = ref.watch(activeIdStateProvider);
|
||||
final requestModel = ref
|
||||
.read(collectionStateNotifierProvider.notifier)
|
||||
.getRequestModel(activeId!);
|
||||
List<FormDataModel> rows = requestModel?.formDataList ?? [];
|
||||
DaviModel<FormDataModel> model = DaviModel<FormDataModel>(
|
||||
rows: rows,
|
||||
columns: [
|
||||
DaviColumn(
|
||||
name: 'Key',
|
||||
grow: 1,
|
||||
cellBuilder: (_, row) {
|
||||
int idx = row.index;
|
||||
return SizedBox(
|
||||
child: FormDataField(
|
||||
keyId: "$activeId-$idx-form-v-${widget.seed}",
|
||||
initialValue: rows[idx].name,
|
||||
hintText: " 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: "");
|
||||
_onFieldChange(activeId);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
sortable: false,
|
||||
),
|
||||
DaviColumn(
|
||||
width: 10,
|
||||
cellBuilder: (_, row) {
|
||||
return Text(
|
||||
"=",
|
||||
style: kCodeStyle,
|
||||
);
|
||||
},
|
||||
),
|
||||
DaviColumn(
|
||||
name: 'Value',
|
||||
grow: 4,
|
||||
cellBuilder: (_, row) {
|
||||
int idx = row.index;
|
||||
return rows[idx].type == FormDataType.file
|
||||
? Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: kPs8,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButtonTheme(
|
||||
data: const ElevatedButtonThemeData(),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
FilePickerResult? pickedResult =
|
||||
await FilePicker.platform.pickFiles();
|
||||
if (pickedResult != null &&
|
||||
pickedResult.files.isNotEmpty) {
|
||||
rows[idx] = rows[idx].copyWith(
|
||||
value: pickedResult.files.first.path,
|
||||
);
|
||||
_onFieldChange(activeId);
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.snippet_folder_rounded,
|
||||
size: 18,
|
||||
),
|
||||
label: Text(
|
||||
rows[idx].type == FormDataType.file
|
||||
? (rows[idx].value != null
|
||||
? rows[idx].value.toString()
|
||||
: "Select File")
|
||||
: "Select File",
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: kTextStyleButton.copyWith(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: CellField(
|
||||
keyId: "$activeId-$idx-form-v-${widget.seed}",
|
||||
initialValue: rows[idx].value,
|
||||
hintText: " 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: () {
|
||||
widget.onFormDataRemove();
|
||||
if (rows.length == 1) {
|
||||
setState(() {
|
||||
rows = [
|
||||
kFormDataEmptyModel,
|
||||
];
|
||||
});
|
||||
} else {
|
||||
rows.removeAt(row.index);
|
||||
}
|
||||
_onFieldChange(activeId);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
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>(model),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 30),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
rows.add(kFormDataEmptyModel);
|
||||
_onFieldChange(activeId);
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text(
|
||||
"Add Form Data",
|
||||
style: kTextStyleButton,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _onFieldChange(String activeId) {
|
||||
List<FormDataModel> formDataList =
|
||||
ref.read(collectionStateNotifierProvider)?[activeId]?.formDataList ??
|
||||
[];
|
||||
ref.read(collectionStateNotifierProvider.notifier).update(
|
||||
activeId,
|
||||
formDataList: formDataList,
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user