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 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>
&nbsp;
{renderButtonText(filnames)}
{renderButtonText(filenames)}
</div>
) : (
<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 { 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) =>

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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