mirror of
https://github.com/usebruno/bruno.git
synced 2024-12-23 07:09:01 +01:00
feat(#1130): file upload schema updates
This commit is contained in:
parent
634f9ca4a2
commit
09e7ea0d4d
@ -1,15 +1,22 @@
|
||||
import React from 'react';
|
||||
import path from 'path';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { browseFiles } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { IconX } from '@tabler/icons';
|
||||
import { isWindowsOS } from 'utils/common/platform';
|
||||
|
||||
const FilePickerEditor = ({ value, onChange, collection }) => {
|
||||
value = value || [];
|
||||
const dispatch = useDispatch();
|
||||
const filnames = value
|
||||
.split('|')
|
||||
const filenames = value
|
||||
.filter((v) => v != null && v != '')
|
||||
.map((v) => v.split('\\').pop());
|
||||
const title = filnames.map((v) => `- ${v}`).join('\n');
|
||||
.map((v) => {
|
||||
const separator = isWindowsOS() ? '\\' : '/';
|
||||
return v.split(separator).pop();
|
||||
});
|
||||
|
||||
// title is shown when hovering over the button
|
||||
const title = filenames.map((v) => `- ${v}`).join('\n');
|
||||
|
||||
const browse = () => {
|
||||
dispatch(browseFiles())
|
||||
@ -20,13 +27,13 @@ const FilePickerEditor = ({ value, onChange, collection }) => {
|
||||
const collectionDir = collection.pathname;
|
||||
|
||||
if (filePath.startsWith(collectionDir)) {
|
||||
return filePath.substring(collectionDir.length + 1);
|
||||
return path.relative(collectionDir, filePath);
|
||||
}
|
||||
|
||||
return filePath;
|
||||
});
|
||||
|
||||
onChange(filePaths.join('|'));
|
||||
onChange(filePaths);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
@ -37,14 +44,14 @@ const FilePickerEditor = ({ value, onChange, collection }) => {
|
||||
onChange('');
|
||||
};
|
||||
|
||||
const renderButtonText = (filnames) => {
|
||||
if (filnames.length == 1) {
|
||||
return filnames[0];
|
||||
const renderButtonText = (filenames) => {
|
||||
if (filenames.length == 1) {
|
||||
return filenames[0];
|
||||
}
|
||||
return filnames.length + ' files selected';
|
||||
return filenames.length + ' files selected';
|
||||
};
|
||||
|
||||
return filnames.length > 0 ? (
|
||||
return filenames.length > 0 ? (
|
||||
<div
|
||||
className="btn btn-secondary px-1"
|
||||
style={{ fontWeight: 400, width: '100%', textOverflow: 'ellipsis', overflowX: 'hidden' }}
|
||||
@ -54,7 +61,7 @@ const FilePickerEditor = ({ value, onChange, collection }) => {
|
||||
<IconX size={18} />
|
||||
</button>
|
||||
|
||||
{renderButtonText(filnames)}
|
||||
{renderButtonText(filenames)}
|
||||
</div>
|
||||
) : (
|
||||
<button className="btn btn-secondary px-1" style={{ width: '100%' }} onClick={browse}>
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import FilePickerEditor from 'components/FilePickerEditor/index';
|
||||
import FilePickerEditor from 'components/FilePickerEditor';
|
||||
|
||||
const MultipartFormParams = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@ -23,7 +23,8 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
dispatch(
|
||||
addMultipartFormParam({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
collectionUid: collection.uid,
|
||||
type: 'text'
|
||||
})
|
||||
);
|
||||
};
|
||||
@ -33,7 +34,7 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
addMultipartFormParam({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
isFile: true
|
||||
type: 'file'
|
||||
})
|
||||
);
|
||||
};
|
||||
@ -103,7 +104,7 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
{param.isFile === true ? (
|
||||
{param.type === 'file' ? (
|
||||
<FilePickerEditor
|
||||
value={param.value}
|
||||
onChange={(newValue) =>
|
||||
|
@ -617,7 +617,7 @@ export const collectionsSlice = createSlice({
|
||||
item.draft.request.body.multipartForm = item.draft.request.body.multipartForm || [];
|
||||
item.draft.request.body.multipartForm.push({
|
||||
uid: uuid(),
|
||||
isFile: action.payload.isFile ?? false,
|
||||
type: action.payload.type,
|
||||
name: '',
|
||||
value: '',
|
||||
description: '',
|
||||
@ -638,7 +638,7 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
const param = find(item.draft.request.body.multipartForm, (p) => p.uid === action.payload.param.uid);
|
||||
if (param) {
|
||||
param.isFile = action.payload.param.isFile;
|
||||
param.type = action.payload.param.type;
|
||||
param.name = action.payload.param.name;
|
||||
param.value = action.payload.param.value;
|
||||
param.description = action.payload.param.description;
|
||||
|
@ -256,6 +256,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
return map(params, (param) => {
|
||||
return {
|
||||
uid: param.uid,
|
||||
type: param.type,
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
@ -520,10 +521,6 @@ export const refreshUidsInItem = (item) => {
|
||||
return item;
|
||||
};
|
||||
|
||||
export const isLocalCollection = (collection) => {
|
||||
return collection.pathname ? true : false;
|
||||
};
|
||||
|
||||
export const deleteUidsInItem = (item) => {
|
||||
delete item.uid;
|
||||
const params = get(item, 'request.params', []);
|
||||
|
@ -11,10 +11,6 @@ export const isElectron = () => {
|
||||
return window.ipcRenderer ? true : false;
|
||||
};
|
||||
|
||||
export const isLocalCollection = (collection) => {
|
||||
return collection.pathname ? true : false;
|
||||
};
|
||||
|
||||
export const resolveRequestFilename = (name) => {
|
||||
return `${trim(name)}.bru`;
|
||||
};
|
||||
|
@ -71,6 +71,18 @@ export const transformItemsInCollection = (collection) => {
|
||||
}
|
||||
|
||||
delete item.request.query;
|
||||
|
||||
// from 5 feb 2024, multipartFormData needs to have a type
|
||||
// this was introduced when we added support for file uploads
|
||||
// below logic is to make older collection exports backward compatible
|
||||
let multipartFormData = _.get(item, 'request.body.multipartForm');
|
||||
if (multipartFormData) {
|
||||
_.each(multipartFormData, (form) => {
|
||||
if (!form.type) {
|
||||
form.type = 'text';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (item.items && item.items.length) {
|
||||
|
@ -152,6 +152,7 @@ const transformInsomniaRequestItem = (request, index, allRequests) => {
|
||||
each(request.body.params, (param) => {
|
||||
brunoRequestItem.request.body.multipartForm.push({
|
||||
uid: uuid(),
|
||||
type: 'text',
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
|
@ -168,6 +168,7 @@ const transformOpenapiRequestItem = (request) => {
|
||||
each(bodySchema.properties || {}, (prop, name) => {
|
||||
brunoRequestItem.request.body.multipartForm.push({
|
||||
uid: uuid(),
|
||||
type: 'text',
|
||||
name: name,
|
||||
value: '',
|
||||
description: prop.description || '',
|
||||
|
@ -144,8 +144,9 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => {
|
||||
if (bodyMode === 'formdata') {
|
||||
brunoRequestItem.request.body.mode = 'multipartForm';
|
||||
each(i.request.body.formdata, (param) => {
|
||||
brunoRequestItem.request.body.formUrlEncoded.push({
|
||||
brunoRequestItem.request.body.multipartForm.push({
|
||||
uid: uuid(),
|
||||
type: 'text',
|
||||
name: param.key,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
|
@ -109,7 +109,7 @@ const prepareRequest = (request, collectionRoot) => {
|
||||
each(enabledParams, (p) => (params[p.name] = p.value));
|
||||
axiosRequest.headers['content-type'] = 'multipart/form-data';
|
||||
axiosRequest.data = params;
|
||||
// TODO is it needed here as well ?
|
||||
// TODO: Add support for file uploads
|
||||
}
|
||||
|
||||
if (request.body.mode === 'graphql') {
|
||||
|
@ -40,7 +40,7 @@ const runSingleRequest = async function (
|
||||
// make axios work in node using form data
|
||||
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
|
||||
if (request.headers && request.headers['content-type'] === 'multipart/form-data') {
|
||||
// TODO remove ?
|
||||
// TODO: Add support for file uploads
|
||||
const form = new FormData();
|
||||
forOwn(request.data, (value, key) => {
|
||||
form.append(key, value);
|
||||
@ -204,7 +204,7 @@ const runSingleRequest = async function (
|
||||
console.log(chalk.red(stripExtension(filename)) + chalk.dim(` (${err.message})`));
|
||||
return {
|
||||
test: {
|
||||
filename: filename,
|
||||
filename: filename
|
||||
},
|
||||
request: {
|
||||
method: request.method,
|
||||
@ -327,7 +327,7 @@ const runSingleRequest = async function (
|
||||
|
||||
return {
|
||||
test: {
|
||||
filename: filename,
|
||||
filename: filename
|
||||
},
|
||||
request: {
|
||||
method: request.method,
|
||||
@ -351,7 +351,7 @@ const runSingleRequest = async function (
|
||||
console.log(chalk.red(stripExtension(filename)) + chalk.dim(` (${err.message})`));
|
||||
return {
|
||||
test: {
|
||||
filename: filename,
|
||||
filename: filename
|
||||
},
|
||||
request: {
|
||||
method: null,
|
||||
|
@ -1,21 +1,18 @@
|
||||
const { get, each, filter, forOwn, extend } = require('lodash');
|
||||
const { get, each, filter, extend } = require('lodash');
|
||||
const decomment = require('decomment');
|
||||
const FormData = require('form-data');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const parseFormData = (datas, collectionPath) => {
|
||||
// make axios work in node using form data
|
||||
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
|
||||
const form = new FormData();
|
||||
datas.forEach((item) => {
|
||||
const value = item.value;
|
||||
const name = item.name;
|
||||
if (item.isFile === true) {
|
||||
const filePaths = value
|
||||
.toString()
|
||||
.replace(/^@file\(/, '')
|
||||
.replace(/\)$/, '')
|
||||
.split('|');
|
||||
|
||||
if (item.type === 'file') {
|
||||
const filePaths = value || [];
|
||||
filePaths.forEach((filePath) => {
|
||||
let trimmedFilePath = filePath.trim();
|
||||
if (!path.isAbsolute(trimmedFilePath)) {
|
||||
@ -175,8 +172,6 @@ const prepareRequest = (request, collectionRoot, collectionPath) => {
|
||||
}
|
||||
|
||||
if (request.body.mode === 'multipartForm') {
|
||||
// make axios work in node using form data
|
||||
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
|
||||
const enabledParams = filter(request.body.multipartForm, (p) => p.enabled);
|
||||
const form = parseFormData(enabledParams, collectionPath);
|
||||
extend(axiosRequest.headers, form.getHeaders());
|
||||
|
@ -132,9 +132,11 @@ const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) =
|
||||
const pairs = mapPairListToKeyValPairs(pairList, parseEnabled);
|
||||
|
||||
return pairs.map((pair) => {
|
||||
pair.type = 'text';
|
||||
if (pair.value.startsWith('@file(') && pair.value.endsWith(')')) {
|
||||
pair.isFile = true;
|
||||
pair.value = pair.value.replace(/^@file\(/, '').replace(/\)$/, '');
|
||||
let filestr = pair.value.replace(/^@file\(/, '').replace(/\)$/, '');
|
||||
pair.type = 'file';
|
||||
pair.value = filestr.split('|');
|
||||
}
|
||||
return pair;
|
||||
});
|
||||
|
@ -188,9 +188,17 @@ ${indentString(body.sparql)}
|
||||
multipartForms
|
||||
.map((item) => {
|
||||
const enabled = item.enabled ? '' : '~';
|
||||
const value = item.isFile ? `@file(${item.value})` : item.value;
|
||||
|
||||
return `${enabled}${item.name}: ${value}`;
|
||||
if (item.type === 'text') {
|
||||
return `${enabled}${item.name}: ${item.value}`;
|
||||
}
|
||||
|
||||
if (item.type === 'file') {
|
||||
let filepaths = item.value || [];
|
||||
let filestr = filepaths.join('|');
|
||||
const value = `@file(${filestr})`;
|
||||
return `${enabled}${item.name}: ${value}`;
|
||||
}
|
||||
})
|
||||
.join('\n')
|
||||
)}`;
|
||||
|
@ -24,7 +24,6 @@ const environmentsSchema = Yup.array().of(environmentSchema);
|
||||
|
||||
const keyValueSchema = Yup.object({
|
||||
uid: uidSchema,
|
||||
isFile: Yup.boolean().nullable(),
|
||||
name: Yup.string().nullable(),
|
||||
value: Yup.string().nullable(),
|
||||
description: Yup.string().nullable(),
|
||||
@ -38,8 +37,11 @@ const varsSchema = Yup.object({
|
||||
name: Yup.string().nullable(),
|
||||
value: Yup.string().nullable(),
|
||||
description: Yup.string().nullable(),
|
||||
local: Yup.boolean(),
|
||||
enabled: Yup.boolean()
|
||||
enabled: Yup.boolean(),
|
||||
|
||||
// todo
|
||||
// anoop(4 feb 2023) - nobody uses this, and it needs to be removed
|
||||
local: Yup.boolean()
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
@ -56,6 +58,21 @@ const graphqlBodySchema = Yup.object({
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
const multipartFormSchema = Yup.object({
|
||||
uid: uidSchema,
|
||||
type: Yup.string().oneOf(['file', 'text']).required('type is required'),
|
||||
name: Yup.string().nullable(),
|
||||
value: Yup.mixed().when('type', {
|
||||
is: 'file',
|
||||
then: Yup.array().of(Yup.string().nullable()).nullable(),
|
||||
otherwise: Yup.string().nullable()
|
||||
}),
|
||||
description: Yup.string().nullable(),
|
||||
enabled: Yup.boolean()
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
const requestBodySchema = Yup.object({
|
||||
mode: Yup.string()
|
||||
.oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm', 'graphql', 'sparql'])
|
||||
@ -65,7 +82,7 @@ const requestBodySchema = Yup.object({
|
||||
xml: Yup.string().nullable(),
|
||||
sparql: Yup.string().nullable(),
|
||||
formUrlEncoded: Yup.array().of(keyValueSchema).nullable(),
|
||||
multipartForm: Yup.array().of(keyValueSchema).nullable(),
|
||||
multipartForm: Yup.array().of(multipartFormSchema).nullable(),
|
||||
graphql: graphqlBodySchema.nullable()
|
||||
})
|
||||
.noUnknown(true)
|
||||
|
Loading…
Reference in New Issue
Block a user