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