feat(#1130): file upload schema updates

This commit is contained in:
Anoop M D 2024-02-05 02:52:03 +05:30
parent 634f9ca4a2
commit 09e7ea0d4d
15 changed files with 88 additions and 50 deletions

View File

@ -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>
&nbsp; &nbsp;
{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}>

View File

@ -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) =>

View File

@ -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;

View File

@ -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', []);

View File

@ -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`;
}; };

View File

@ -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) {

View File

@ -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,

View File

@ -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 || '',

View File

@ -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,

View File

@ -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') {

View File

@ -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,

View File

@ -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());

View File

@ -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;
}); });

View File

@ -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')
)}`; )}`;

View File

@ -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)