mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 17:38:36 +00:00
Compare commits
5 Commits
fix/sideba
...
feat/theme
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19840f8546 | ||
|
|
d03d8f01a1 | ||
|
|
97c700beba | ||
|
|
b6a27bc66c | ||
|
|
76a2889206 |
9
package-lock.json
generated
9
package-lock.json
generated
@@ -29,7 +29,7 @@
|
||||
"@eslint/compat": "^1.3.2",
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@opencollection/types": "~0.6.0",
|
||||
"@opencollection/types": "~0.7.0",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@storybook/addon-webpack5-compiler-babel": "^4.0.0",
|
||||
@@ -6100,9 +6100,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@opencollection/types": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@opencollection/types/-/types-0.6.0.tgz",
|
||||
"integrity": "sha512-nasB4/1hIZ61xp2dnnZWhdH83f0t800VrSl3G2q+BtHabBqN/IG+j9BMOJg0hYZjAVx+Yhl1njkzUqkiX5+Q0g==",
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@opencollection/types/-/types-0.7.0.tgz",
|
||||
"integrity": "sha512-CSwdaHNPa2bNNBAOy++t6W9gBTExUJZW3aPkWyhAjasusThbvjymD/0uCLR50gCXSs0ezv61jsd19m9x+2DMtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -33081,6 +33081,7 @@
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
"babel-jest": "^29.7.0",
|
||||
"form-data": "^4.0.0",
|
||||
"is-ip": "^5.0.1",
|
||||
"moment": "^2.29.4",
|
||||
"rollup": "3.29.5",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"@eslint/compat": "^1.3.2",
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@opencollection/types": "~0.6.0",
|
||||
"@opencollection/types": "~0.7.0",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@storybook/addon-webpack5-compiler-babel": "^4.0.0",
|
||||
|
||||
@@ -5,7 +5,6 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: ${(props) => props.theme.bg};
|
||||
position: relative;
|
||||
|
||||
.environments-container {
|
||||
|
||||
@@ -3,7 +3,6 @@ import styled from 'styled-components';
|
||||
const StyledWrapper = styled.div`
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: ${(props) => props.theme.bg};
|
||||
border-radius: 4px;
|
||||
|
||||
div.tabs {
|
||||
|
||||
@@ -3,7 +3,6 @@ import styled from 'styled-components';
|
||||
const StyledWrapper = styled.div`
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: ${(props) => props.theme.bg};
|
||||
border-radius: 4px;
|
||||
|
||||
div.tabs {
|
||||
|
||||
@@ -185,7 +185,7 @@ const WSMessagesList = ({ order = -1, messages = [] }) => {
|
||||
|
||||
// sort based on order, seq was newly added and might be missing in some cases and when missing,
|
||||
// the timestamp will be used instead
|
||||
const ordered = messages.toSorted((x, y) => ((x.seq ?? x.timestamp) - (y.seq ?? y.timestamp)) * order);
|
||||
const ordered = messages.toSorted((x, y) => ((x.seq ?? x.timestamp) - (y.seq ?? y.timestamp)) * (-order));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="ws-messages-list flex flex-col">
|
||||
|
||||
@@ -13,6 +13,30 @@ const StyledWrapper = styled.div`
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
border-radius: ${(props) => props.theme.border.radius.md};
|
||||
overflow: hidden;
|
||||
border: 1px solid ${(props) => props.theme.border.border1};
|
||||
|
||||
.preview-label {
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
padding: 0.125rem 0.5rem;
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
font-weight: 500;
|
||||
color: #3b82f6;
|
||||
background-color: rgba(59, 130, 246, 0.1);
|
||||
border: 1px dashed rgba(59, 130, 246, 0.4);
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.features {
|
||||
li {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
@@ -9,6 +9,7 @@ import { IconBook, IconCheck, IconAlertTriangle, IconLoader2 } from '@tabler/ico
|
||||
|
||||
import Modal from 'components/Modal';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import demoImage from './demo.png';
|
||||
import { useApp } from 'providers/App';
|
||||
import { transformCollectionToSaveToExportAsFile, findCollectionByUid, areItemsLoading } from 'utils/collections/index';
|
||||
import { brunoToOpenCollection } from '@usebruno/converters';
|
||||
@@ -141,12 +142,16 @@ const GenerateDocumentation = ({ onClose, collectionUid }) => {
|
||||
<IconBook size={18} />
|
||||
<span>Interactive API Documentation</span>
|
||||
</h3>
|
||||
<p className="description mb-5">
|
||||
Generate a standalone HTML file containing interactive documentation for your API collection.
|
||||
This file can be hosted anywhere or shared with your team.
|
||||
<p className="description mb-4">
|
||||
Generate a standalone HTML file that can be hosted anywhere or shared with your team.
|
||||
</p>
|
||||
|
||||
<ul className="features flex flex-col list-none gap-2.5 p-0 mb-5">
|
||||
<div className="preview-container relative mb-4">
|
||||
<span className="preview-label absolute">Sample Output</span>
|
||||
<img src={demoImage} alt="Documentation preview" className="preview-image" />
|
||||
</div>
|
||||
|
||||
<ul className="features flex flex-col list-none gap-2 p-0 mb-4">
|
||||
{FEATURES.map((feature) => (
|
||||
<li key={feature} className="flex items-center gap-2.5">
|
||||
<IconCheck size={16} className="check-icon flex-shrink-0" />
|
||||
@@ -156,7 +161,7 @@ const GenerateDocumentation = ({ onClose, collectionUid }) => {
|
||||
</ul>
|
||||
|
||||
<p className="note m-0">
|
||||
The generated file does not embed all assets. It loads OpenCollection’s JavaScript and CSS files from a CDN when viewing docs, which requires an internet connection.
|
||||
The generated file loads OpenCollection's JavaScript and CSS files from a CDN, which requires an internet connection.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -14,6 +14,18 @@ const StyledWrapper = styled.div`
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
}
|
||||
|
||||
.discussion-link {
|
||||
margin-left: 0.5rem;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.textLink};
|
||||
cursor: pointer;
|
||||
font-weight: 400;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.report-issue-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -224,7 +224,19 @@ const CreateCollection = ({ onClose, defaultLocation: propDefaultLocation }) =>
|
||||
</p>
|
||||
</Help>
|
||||
{formik.values.format === 'yml' && (
|
||||
<span className="beta-badge">Beta</span>
|
||||
<>
|
||||
<span className="beta-badge">Beta</span>
|
||||
<a
|
||||
href="#"
|
||||
className="discussion-link"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.open('https://github.com/usebruno/bruno/discussions/6634', '_blank', 'noopener,noreferrer');
|
||||
}}
|
||||
>
|
||||
Join the discussion
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</label>
|
||||
<select
|
||||
|
||||
@@ -5,7 +5,6 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: ${(props) => props.theme.bg};
|
||||
position: relative;
|
||||
|
||||
.environments-container {
|
||||
|
||||
@@ -295,7 +295,7 @@ const darkPastelTheme = {
|
||||
},
|
||||
body: {
|
||||
color: colors.TEXT,
|
||||
bg: colors.GRAY_1
|
||||
bg: colors.GRAY_2
|
||||
},
|
||||
input: {
|
||||
bg: 'transparent',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const { interpolate } = require('@usebruno/common');
|
||||
const { each, forOwn, cloneDeep, find } = require('lodash');
|
||||
const FormData = require('form-data');
|
||||
const { isFormData } = require('@usebruno/common').utils;
|
||||
|
||||
const getContentType = (headers = {}) => {
|
||||
let contentType = '';
|
||||
@@ -87,7 +87,7 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
}));
|
||||
}
|
||||
} else if (contentType === 'multipart/form-data') {
|
||||
if (Array.isArray(request?.data) && !(request.data instanceof FormData)) {
|
||||
if (Array.isArray(request?.data) && !isFormData(request.data)) {
|
||||
try {
|
||||
request.data = request?.data?.map((d) => ({
|
||||
...d,
|
||||
|
||||
@@ -3,7 +3,6 @@ const chalk = require('chalk');
|
||||
const decomment = require('decomment');
|
||||
const fs = require('fs');
|
||||
const { forOwn, isUndefined, isNull, each, extend, get, compact } = require('lodash');
|
||||
const FormData = require('form-data');
|
||||
const prepareRequest = require('./prepare-request');
|
||||
const interpolateVars = require('./interpolate-vars');
|
||||
const { interpolateString } = require('./interpolate-string');
|
||||
@@ -25,7 +24,7 @@ const { NtlmClient } = require('axios-ntlm');
|
||||
const { addDigestInterceptor } = require('@usebruno/requests');
|
||||
const { getCACertificates } = require('@usebruno/requests');
|
||||
const { getOAuth2Token } = require('../utils/oauth2');
|
||||
const { encodeUrl, buildFormUrlEncodedPayload, extractPromptVariables } = require('@usebruno/common').utils;
|
||||
const { encodeUrl, buildFormUrlEncodedPayload, extractPromptVariables, isFormData } = require('@usebruno/common').utils;
|
||||
|
||||
const onConsoleLog = (type, args) => {
|
||||
console[type](...args);
|
||||
@@ -429,7 +428,7 @@ const runSingleRequest = async function (
|
||||
}
|
||||
|
||||
if (contentTypeHeader && request.headers[contentTypeHeader] === 'multipart/form-data') {
|
||||
if (!(request?.data instanceof FormData)) {
|
||||
if (!isFormData(request?.data)) {
|
||||
request._originalMultipartData = request.data;
|
||||
request.collectionPath = collectionPath;
|
||||
let form = createFormData(request.data, collectionPath);
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
"babel-jest": "^29.7.0",
|
||||
"form-data": "^4.0.0",
|
||||
"is-ip": "^5.0.1",
|
||||
"moment": "^2.29.4",
|
||||
"rollup": "3.29.5",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { buildFormUrlEncodedPayload } from './form-data';
|
||||
import { buildFormUrlEncodedPayload, isFormData } from './form-data';
|
||||
import FormData from 'form-data';
|
||||
|
||||
describe('buildFormUrlEncodedPayload', () => {
|
||||
it('should handle single key-value pair', () => {
|
||||
@@ -110,3 +111,53 @@ describe('buildFormUrlEncodedPayload', () => {
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isFormData', () => {
|
||||
it('should return true for objects with FormData constructor name', () => {
|
||||
const mockFormData = {
|
||||
constructor: { name: 'FormData' }
|
||||
};
|
||||
expect(isFormData(mockFormData)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for null', () => {
|
||||
expect(isFormData(null)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for undefined', () => {
|
||||
expect(isFormData(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for plain objects', () => {
|
||||
expect(isFormData({})).toBe(false);
|
||||
expect(isFormData({ key: 'value' })).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for arrays', () => {
|
||||
expect(isFormData([])).toBe(false);
|
||||
expect(isFormData([1, 2, 3])).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for primitives', () => {
|
||||
expect(isFormData('string')).toBe(false);
|
||||
expect(isFormData(123)).toBe(false);
|
||||
expect(isFormData(true)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for objects with different constructor names', () => {
|
||||
class CustomClass {}
|
||||
const customObj = new CustomClass();
|
||||
expect(isFormData(customObj)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for objects without constructor', () => {
|
||||
const obj = Object.create(null);
|
||||
expect(isFormData(obj)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for actual FormData instance from form-data library', () => {
|
||||
const formData = new FormData();
|
||||
formData.append('key', 'value');
|
||||
expect(isFormData(formData)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,3 +31,15 @@ export const buildFormUrlEncodedPayload = (params: Array<{ name: string; value:
|
||||
|
||||
return resultParams.toString();
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if the given object is a FormData instance.
|
||||
* Supports native FormData (Node 18+, browser) and the 'form-data' npm package.
|
||||
* @param obj - Object to check.
|
||||
* @returns True if obj is a FormData instance, false otherwise.
|
||||
*/
|
||||
export const isFormData = (obj: unknown): boolean => {
|
||||
// Check constructor name (works for both native FormData and form-data npm package)
|
||||
// todo: checking constructor.name can produce false positives for objects that have a constructor.name property set to 'FormData', but this is rare.
|
||||
return obj?.constructor?.name === 'FormData';
|
||||
};
|
||||
|
||||
@@ -5,7 +5,8 @@ export {
|
||||
} from './url';
|
||||
|
||||
export {
|
||||
buildFormUrlEncodedPayload
|
||||
buildFormUrlEncodedPayload,
|
||||
isFormData
|
||||
} from './form-data';
|
||||
|
||||
export {
|
||||
|
||||
@@ -5,7 +5,6 @@ const qs = require('qs');
|
||||
const decomment = require('decomment');
|
||||
const contentDispositionParser = require('content-disposition');
|
||||
const mime = require('mime-types');
|
||||
const FormData = require('form-data');
|
||||
const { ipcMain } = require('electron');
|
||||
const { each, get, extend, cloneDeep, merge } = require('lodash');
|
||||
const { NtlmClient } = require('axios-ntlm');
|
||||
@@ -36,7 +35,7 @@ const { cookiesStore } = require('../../store/cookies');
|
||||
const registerGrpcEventHandlers = require('./grpc-event-handlers');
|
||||
const { registerWsEventHandlers } = require('./ws-event-handlers');
|
||||
const { getCertsAndProxyConfig } = require('./cert-utils');
|
||||
const { buildFormUrlEncodedPayload } = require('@usebruno/common').utils;
|
||||
const { buildFormUrlEncodedPayload, isFormData } = require('@usebruno/common').utils;
|
||||
|
||||
const ERROR_OCCURRED_WHILE_EXECUTING_REQUEST = 'Error occurred while executing the request!';
|
||||
|
||||
@@ -474,7 +473,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
|
||||
if (contentTypeHeader && request.headers[contentTypeHeader] === 'multipart/form-data') {
|
||||
if (!(request.data instanceof FormData)) {
|
||||
if (!isFormData(request.data)) {
|
||||
request._originalMultipartData = request.data;
|
||||
request.collectionPath = collectionPath;
|
||||
let form = createFormData(request.data, collectionPath);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const { interpolate } = require('@usebruno/common');
|
||||
const { each, forOwn, cloneDeep } = require('lodash');
|
||||
const FormData = require('form-data');
|
||||
const { isFormData } = require('@usebruno/common').utils;
|
||||
|
||||
const getContentType = (headers = {}) => {
|
||||
let contentType = '';
|
||||
@@ -132,7 +132,7 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
}));
|
||||
}
|
||||
} else if (contentType === 'multipart/form-data') {
|
||||
if (Array.isArray(request?.data) && !(request.data instanceof FormData)) {
|
||||
if (Array.isArray(request?.data) && !isFormData(request.data)) {
|
||||
try {
|
||||
request.data = request?.data?.map((d) => ({
|
||||
...d,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const { customAlphabet } = require('nanoid');
|
||||
const iconv = require('iconv-lite');
|
||||
const { cloneDeep } = require('lodash');
|
||||
const FormData = require('form-data');
|
||||
const { formatMultipartData } = require('./form-data');
|
||||
const { isFormData } = require('@usebruno/common').utils;
|
||||
|
||||
// a customized version of nanoid without using _ and -
|
||||
const uuid = () => {
|
||||
@@ -135,7 +135,7 @@ const parseDataFromRequest = (request) => {
|
||||
// File uploads are redacted, multipart FormData is formatted from original data for readability, and other types are stringified as-is.
|
||||
if (request.mode === 'file') {
|
||||
requestDataString = '<request body redacted>';
|
||||
} else if (request?.data instanceof FormData && Array.isArray(request._originalMultipartData)) {
|
||||
} else if (isFormData(request?.data) && Array.isArray(request._originalMultipartData)) {
|
||||
const boundary = request.data._boundary || 'boundary';
|
||||
requestDataString = formatMultipartData(request._originalMultipartData, boundary);
|
||||
} else {
|
||||
|
||||
@@ -81,6 +81,10 @@ export const toOpenCollectionBody = (body: BrunoHttpRequestBody | null | undefin
|
||||
value: entry.value || (entry.type === 'file' ? [] : '')
|
||||
};
|
||||
|
||||
if (entry?.contentType?.trim().length) {
|
||||
multipartEntry.contentType = entry.contentType;
|
||||
}
|
||||
|
||||
if (entry?.description?.trim().length) {
|
||||
multipartEntry.description = entry.description;
|
||||
}
|
||||
@@ -200,7 +204,7 @@ export const toBrunoBody = (body: HttpRequestBody | null | undefined): BrunoHttp
|
||||
type: entry.type,
|
||||
name: entry.name || '',
|
||||
value: entry.value || (entry.type === 'file' ? [] : ''),
|
||||
contentType: null,
|
||||
contentType: entry.contentType || null,
|
||||
enabled: entry.disabled !== true
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user