add: beta tag for opencollection & fix create collection location behaviour (#6594)

* add: beta tag for opencollection

* fixes

* fix
This commit is contained in:
naman-bruno
2026-01-01 17:04:34 +05:30
committed by GitHub
parent 2c973bbd35
commit 1a4a30c8f2
5 changed files with 192 additions and 166 deletions

View File

@@ -0,0 +1,47 @@
import React, { useId } from 'react';
import styled from 'styled-components';
const StyledSvg = styled.svg`
.icon-stroke {
stroke: ${(props) => props.theme.text};
}
.icon-fill {
fill: ${(props) => props.theme.text};
}
`;
const OpenCollectionIcon = ({ size = 28 }) => {
const clipId = useId();
return (
<StyledSvg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 567 567"
preserveAspectRatio="xMidYMid meet"
>
<g transform="matrix(1, 0, 0, 1, 93, 56)">
<clipPath id={clipId}>
<rect x="0" width="418" y="0" height="455" />
</clipPath>
<g clipPath={`url(#${clipId})`}>
<path
className="icon-fill"
d="M 249.175781 222.132812 C 249.175781 224.011719 249.085938 225.890625 248.90625 227.761719 C 248.722656 229.632812 248.453125 231.492188 248.09375 233.335938 C 247.738281 235.179688 247.289062 237 246.753906 238.800781 C 246.222656 240.601562 245.601562 242.367188 244.898438 244.105469 C 244.195312 245.84375 243.40625 247.542969 242.539062 249.199219 C 241.671875 250.859375 240.726562 252.46875 239.707031 254.035156 C 238.683594 255.597656 237.589844 257.105469 236.421875 258.558594 C 235.253906 260.011719 234.019531 261.40625 232.71875 262.734375 C 231.417969 264.0625 230.054688 265.324219 228.632812 266.519531 C 227.210938 267.710938 225.734375 268.832031 224.203125 269.875 C 222.671875 270.921875 221.097656 271.886719 219.472656 272.773438 C 217.851562 273.660156 216.1875 274.460938 214.488281 275.179688 C 212.789062 275.902344 211.058594 276.535156 209.296875 277.078125 C 207.535156 277.625 205.753906 278.082031 203.949219 278.449219 C 202.144531 278.816406 200.324219 279.089844 198.492188 279.277344 C 196.664062 279.460938 194.828125 279.550781 192.988281 279.550781 C 191.144531 279.550781 189.308594 279.460938 187.480469 279.277344 C 185.648438 279.089844 183.828125 278.816406 182.023438 278.449219 C 180.21875 278.082031 178.4375 277.625 176.675781 277.078125 C 174.914062 276.535156 173.183594 275.902344 171.484375 275.179688 C 169.785156 274.460938 168.121094 273.660156 166.5 272.773438 C 164.875 271.886719 163.300781 270.921875 161.769531 269.875 C 160.238281 268.832031 158.761719 267.710938 157.339844 266.519531 C 155.917969 265.324219 154.554688 264.0625 153.253906 262.734375 C 151.953125 261.40625 150.71875 260.011719 149.550781 258.558594 C 148.386719 257.105469 147.289062 255.597656 146.265625 254.035156 C 145.246094 252.46875 144.300781 250.859375 143.433594 249.199219 C 142.566406 247.542969 141.78125 245.84375 141.074219 244.105469 C 140.371094 242.367188 139.75 240.601562 139.21875 238.800781 C 138.683594 237 138.238281 235.179688 137.878906 233.335938 C 137.519531 231.492188 137.25 229.632812 137.070312 227.761719 C 136.886719 225.890625 136.796875 224.011719 136.796875 222.132812 C 136.796875 220.253906 136.886719 218.375 137.070312 216.503906 C 137.25 214.632812 137.519531 212.773438 137.878906 210.929688 C 138.238281 209.085938 138.683594 207.265625 139.21875 205.464844 C 139.75 203.664062 140.371094 201.898438 141.074219 200.160156 C 141.78125 198.421875 142.566406 196.722656 143.433594 195.066406 C 144.300781 193.40625 145.246094 191.796875 146.265625 190.230469 C 147.289062 188.667969 148.386719 187.160156 149.550781 185.707031 C 150.71875 184.253906 151.953125 182.859375 153.253906 181.53125 C 154.554688 180.203125 155.917969 178.941406 157.339844 177.746094 C 158.761719 176.554688 160.238281 175.433594 161.769531 174.390625 C 163.300781 173.34375 164.875 172.378906 166.5 171.492188 C 168.121094 170.605469 169.785156 169.804688 171.484375 169.085938 C 173.183594 168.363281 174.914062 167.730469 176.675781 167.1875 C 178.4375 166.640625 180.21875 166.183594 182.023438 165.816406 C 183.828125 165.449219 185.648438 165.175781 187.480469 164.988281 C 189.308594 164.804688 191.144531 164.714844 192.988281 164.714844 C 194.828125 164.714844 196.664062 164.804688 198.492188 164.988281 C 200.324219 165.175781 202.144531 165.449219 203.949219 165.816406 C 205.753906 166.183594 207.535156 166.640625 209.296875 167.1875 C 211.058594 167.730469 212.789062 168.363281 214.488281 169.085938 C 216.1875 169.804688 217.851562 170.605469 219.472656 171.492188 C 221.097656 172.378906 222.671875 173.34375 224.203125 174.390625 C 225.734375 175.433594 227.210938 176.554688 228.632812 177.746094 C 230.054688 178.941406 231.417969 180.203125 232.71875 181.53125 C 234.019531 182.859375 235.253906 184.253906 236.421875 185.707031 C 237.589844 187.160156 238.683594 188.667969 239.707031 190.230469 C 240.726562 191.796875 241.671875 193.40625 242.539062 195.066406 C 243.40625 196.722656 244.195312 198.421875 244.898438 200.160156 C 245.601562 201.898438 246.222656 203.664062 246.753906 205.464844 C 247.289062 207.265625 247.738281 209.085938 248.09375 210.929688 C 248.453125 212.773438 248.722656 214.632812 248.90625 216.503906 C 249.085938 218.375 249.175781 220.253906 249.175781 222.132812 Z M 249.175781 222.132812"
fillOpacity="1"
fillRule="nonzero"
/>
<path
className="icon-fill"
d="M 331.925781 84.105469 C 304.367188 55.941406 269.136719 36.925781 230.835938 29.546875 C 192.535156 22.164062 152.945312 26.757812 117.242188 42.726562 C 81.535156 58.699219 51.375 85.304688 30.695312 119.066406 C 10.015625 152.828125 -0.214844 192.179688 1.332031 231.980469 C 2.882812 271.78125 16.140625 310.175781 39.375 342.15625 C 62.613281 374.132812 94.746094 398.207031 131.582031 411.230469 C 168.414062 424.25 208.234375 425.621094 245.839844 415.152344 C 283.445312 404.683594 317.089844 382.871094 342.375 352.558594 L 265.257812 285.382812 C 253.199219 299.839844 237.152344 310.246094 219.214844 315.238281 C 201.273438 320.230469 182.28125 319.578125 164.710938 313.367188 C 147.140625 307.15625 131.8125 295.671875 120.730469 280.417969 C 109.644531 265.164062 103.320312 246.851562 102.582031 227.867188 C 101.84375 208.882812 106.722656 190.109375 116.589844 174.007812 C 126.453125 157.898438 140.839844 145.210938 157.871094 137.59375 C 174.902344 129.972656 193.785156 127.78125 212.054688 131.304688 C 230.324219 134.824219 247.128906 143.894531 260.277344 157.328125 Z M 331.925781 84.105469"
fillOpacity="1"
fillRule="nonzero"
/>
</g>
</g>
</StyledSvg>
);
};
export default OpenCollectionIcon;

View File

@@ -27,7 +27,22 @@ const StyledWrapper = styled.div`
}
}
}
.beta-badge-corner {
position: absolute;
top: 0;
right: 0;
padding: 0.25rem 0.5rem;
font-size: 0.625rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.025em;
background-color: ${(props) => rgba(props.theme.colors.text.yellow, 0.15)};
color: ${(props) => props.theme.colors.text.yellow};
border-top-right-radius: ${(props) => props.theme.border.radius.base};
border-bottom-left-radius: ${(props) => props.theme.border.radius.base};
}
.share-button {
display: flex;
border-radius: ${(props) => props.theme.border.radius.base};

View File

@@ -1,8 +1,9 @@
import React, { useMemo } from 'react';
import Modal from 'components/Modal';
import { IconUpload, IconLoader2, IconAlertTriangle, IconFileExport } from '@tabler/icons';
import { IconUpload, IconLoader2, IconAlertTriangle } from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
import Bruno from 'components/Bruno';
import OpenCollectionIcon from 'components/Icons/OpenCollectionIcon';
import exportBrunoCollection from 'utils/collections/export';
import exportPostmanCollection from 'utils/exporters/postman-collection';
import exportOpenCollection from 'utils/exporters/opencollection';
@@ -86,18 +87,19 @@ const ShareCollection = ({ onClose, collectionUid }) => {
</div>
<div
className={`share-button ${
className={`share-button relative ${
isCollectionLoading
? 'opacity-50 cursor-not-allowed'
: 'cursor-pointer'
}`}
onClick={isCollectionLoading ? undefined : handleExportOpenCollection}
>
<span className="beta-badge-corner">Beta</span>
<div className="mr-3 p-1 rounded-full">
{isCollectionLoading ? (
<IconLoader2 size={28} className="animate-spin" />
) : (
<IconFileExport size={28} strokeWidth={1} />
<OpenCollectionIcon size={28} />
)}
</div>
<div className="flex-1">

View File

@@ -1,10 +1,35 @@
import styled from 'styled-components';
import { rgba } from 'polished';
const StyledWrapper = styled.div`
.advanced-options {
.caret {
color: ${(props) => props.theme.textLink};
fill: ${(props) => props.theme.textLink};
.beta-badge {
margin-left: 0.5rem;
padding: 0.125rem 0.5rem;
font-size: 0.625rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.025em;
background-color: ${(props) => rgba(props.theme.colors.text.yellow, 0.15)};
color: ${(props) => props.theme.colors.text.yellow};
border-radius: ${(props) => props.theme.border.radius.sm};
}
.report-issue-link {
display: inline-flex;
align-items: center;
gap: 0.375rem;
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.textLink};
cursor: pointer;
transition: opacity 0.15s ease;
&:hover {
opacity: 0.8;
text-decoration: underline;
}
svg {
flex-shrink: 0;
}
}
`;

View File

@@ -1,8 +1,7 @@
import React, { useRef, useEffect, forwardRef } from 'react';
import React, { useRef, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import path from 'path';
import { browseDirectory, createCollection } from 'providers/ReduxStore/slices/collections/actions';
import toast from 'react-hot-toast';
import Portal from 'components/Portal';
@@ -10,12 +9,10 @@ import Modal from 'components/Modal';
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
import PathDisplay from 'components/PathDisplay/index';
import { useState } from 'react';
import { IconArrowBackUp, IconEdit, IconCaretDown } from '@tabler/icons';
import { IconArrowBackUp, IconEdit, IconExternalLink } from '@tabler/icons';
import Help from 'components/Help';
import { multiLineMsg } from 'utils/common';
import { formatIpcError } from 'utils/common/error';
import { toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';
import Dropdown from 'components/Dropdown';
import StyledWrapper from './StyledWrapper';
import get from 'lodash/get';
import Button from 'ui/Button';
@@ -27,21 +24,11 @@ const CreateCollection = ({ onClose, defaultLocation: propDefaultLocation }) =>
const workspaceUid = useSelector((state) => state.workspaces?.activeWorkspaceUid);
const [isEditing, toggleEditing] = useState(false);
const preferences = useSelector((state) => state.app.preferences);
const [showExternalLocation, setShowExternalLocation] = useState(false);
const [showAdvanced, setShowAdvanced] = useState(true);
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const activeWorkspace = workspaces.find((w) => w.uid === workspaceUid);
const isDefaultWorkspace = activeWorkspace?.type === 'default';
const hideLocationInput = activeWorkspace && activeWorkspace.type !== 'default' && !!activeWorkspace?.pathname;
const defaultLocation = isDefaultWorkspace ? get(preferences, 'general.defaultCollectionLocation', '') : (activeWorkspace?.pathname ? `${activeWorkspace.pathname}/collections` : '');
const shouldShowAccordion = workspaceUid && hideLocationInput && !isDefaultWorkspace;
const actuallyHideLocationInput = hideLocationInput && !showExternalLocation && !isDefaultWorkspace;
const formik = useFormik({
enableReinitialize: true,
initialValues: {
@@ -63,36 +50,16 @@ const CreateCollection = ({ onClose, defaultLocation: propDefaultLocation }) =>
return isValid ? true : this.createError({ message: validateNameError(value) });
})
.required('folder name is required'),
collectionLocation: actuallyHideLocationInput
? Yup.string() // Optional for workspaces when not using external location
: Yup.string().min(1, 'location is required').required('location is required'),
collectionLocation: Yup.string().min(1, 'location is required').required('location is required'),
format: Yup.string().oneOf(['bru', 'yml'], 'invalid format').required('format is required')
}),
onSubmit: async (values) => {
try {
const currentWorkspace = workspaces.find((w) => w.uid === workspaceUid);
const useExternalLocation = workspaceUid && showExternalLocation && values.collectionLocation;
let collectionLocation = values.collectionLocation;
if (workspaceUid && !useExternalLocation && currentWorkspace && currentWorkspace.type !== 'default') {
collectionLocation = path.join(currentWorkspace.pathname, 'collections');
}
await dispatch(createCollection(values.collectionName,
values.collectionFolderName,
collectionLocation,
values.collectionLocation,
{ format: values.format }));
if (useExternalLocation && currentWorkspace) {
const { ipcRenderer } = window;
const collectionPath = path.join(values.collectionLocation, values.collectionFolderName);
const workspaceCollection = {
name: values.collectionName,
path: collectionPath
};
await ipcRenderer.invoke('renderer:add-collection-to-workspace', currentWorkspace.pathname, workspaceCollection);
}
toast.success('Collection created!');
onClose();
} catch (e) {
@@ -119,20 +86,6 @@ const CreateCollection = ({ onClose, defaultLocation: propDefaultLocation }) =>
}
}, [inputRef]);
const AdvancedOptions = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex mr-2 text-link cursor-pointer items-center">
<button
className="btn-advanced"
type="button"
>
Options
</button>
<IconCaretDown className="caret ml-1" size={14} strokeWidth={2} />
</div>
);
});
return (
<Portal>
<StyledWrapper>
@@ -162,47 +115,43 @@ const CreateCollection = ({ onClose, defaultLocation: propDefaultLocation }) =>
<div className="text-red-500">{formik.errors.collectionName}</div>
) : null}
{!actuallyHideLocationInput && (
<>
<label htmlFor="collection-location" className="font-medium mt-3 flex items-center">
Location
<Help>
<p>
Bruno stores your collections on your computer's filesystem.
</p>
<p className="mt-2">
Choose the location where you want to store this collection.
</p>
</Help>
</label>
<input
id="collection-location"
type="text"
name="collectionLocation"
className="block textbox mt-2 w-full cursor-pointer"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.collectionLocation || ''}
onClick={browse}
onChange={(e) => {
formik.setFieldValue('collectionLocation', e.target.value);
}}
/>
{formik.touched.collectionLocation && formik.errors.collectionLocation ? (
<div className="text-red-500">{formik.errors.collectionLocation}</div>
) : null}
<div className="mt-1">
<span
className="text-link cursor-pointer hover:underline"
onClick={browse}
>
Browse
</span>
</div>
</>
)}
<label htmlFor="collection-location" className="font-medium mt-3 flex items-center">
Location
<Help>
<p>
Bruno stores your collections on your computer's filesystem.
</p>
<p className="mt-2">
Choose the location where you want to store this collection.
</p>
</Help>
</label>
<input
id="collection-location"
type="text"
name="collectionLocation"
className="block textbox mt-2 w-full cursor-pointer"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.collectionLocation || ''}
onClick={browse}
onChange={(e) => {
formik.setFieldValue('collectionLocation', e.target.value);
}}
/>
{formik.touched.collectionLocation && formik.errors.collectionLocation ? (
<div className="text-red-500">{formik.errors.collectionLocation}</div>
) : null}
<div className="mt-1">
<span
className="text-link cursor-pointer hover:underline"
onClick={browse}
>
Browse
</span>
</div>
{formik.values.collectionName?.trim()?.length > 0 && (
<div className="mt-4">
<div className="flex items-center justify-between">
@@ -259,78 +208,66 @@ const CreateCollection = ({ onClose, defaultLocation: propDefaultLocation }) =>
</div>
)}
{showAdvanced && (
<div className="mt-4">
<label htmlFor="format" className="flex items-center font-medium">
File Format
<Help width="300">
<p>
Choose the file format for storing requests in this collection.
</p>
<p className="mt-2">
<strong>OpenCollection (YAML):</strong> Industry-standard YAML format (.yml files)
</p>
<p className="mt-1">
<strong>BRU:</strong> Bruno's native file format (.bru files)
</p>
</Help>
</label>
<select
id="format"
name="format"
className="block textbox mt-2 w-full"
value={formik.values.format}
onChange={formik.handleChange}
>
<option value="yml">OpenCollection (YAML)</option>
<option value="bru">BRU Format (.bru)</option>
</select>
{formik.touched.format && formik.errors.format ? (
<div className="text-red-500">{formik.errors.format}</div>
) : null}
</div>
)}
</div>
<div className="flex justify-between items-center mt-8 bruno-modal-footer">
<div className="flex advanced-options">
<Dropdown onCreate={onDropdownCreate} icon={<AdvancedOptions />} placement="bottom-start">
{shouldShowAccordion && (
<div
className="dropdown-item"
key="create-external-location"
<div className="mt-4">
<label htmlFor="format" className="flex items-center font-medium">
File Format
<Help width="300">
<p>
Choose the file format for storing requests in this collection.
</p>
<p className="mt-2">
<strong>OpenCollection (YAML):</strong> Industry-standard YAML format (.yml files)
</p>
<p className="mt-1">
<strong>BRU:</strong> Bruno's native file format (.bru files)
</p>
</Help>
{formik.values.format === 'yml' && (
<span className="beta-badge">Beta</span>
)}
</label>
<select
id="format"
name="format"
className="block textbox mt-2 w-full"
value={formik.values.format}
onChange={formik.handleChange}
>
<option value="yml">OpenCollection (YAML)</option>
<option value="bru">BRU Format (.bru)</option>
</select>
{formik.touched.format && formik.errors.format ? (
<div className="text-red-500">{formik.errors.format}</div>
) : null}
{formik.values.format === 'yml' && (
<div className="mt-2">
<a
href="#"
className="report-issue-link"
onClick={(e) => {
dropdownTippyRef.current.hide();
setShowExternalLocation(!showExternalLocation);
e.preventDefault();
window.open('https://github.com/usebruno/bruno/discussions/6466', '_blank', 'noopener,noreferrer');
}}
>
{showExternalLocation ? 'Use Default Location' : 'Create in External Location'}
</div>
)}
<div
className="dropdown-item"
key="show-file-format"
onClick={(e) => {
dropdownTippyRef.current.hide();
setShowAdvanced(!showAdvanced);
}}
>
{showAdvanced ? 'Hide File Format' : 'Show File Format'}
<IconExternalLink size={14} strokeWidth={1.5} />
<span>Report an issue</span>
</a>
</div>
</Dropdown>
</div>
<div className="flex justify-end">
<span className="mr-2">
<Button type="button" color="secondary" variant="ghost" onClick={onClose}>
Cancel
</Button>
</span>
<span>
<Button type="submit">
Create
</Button>
</span>
)}
</div>
</div>
<div className="flex justify-end items-center mt-8 bruno-modal-footer">
<span className="mr-2">
<Button type="button" color="secondary" variant="ghost" onClick={onClose}>
Cancel
</Button>
</span>
<span>
<Button type="submit">
Create
</Button>
</span>
</div>
</form>
</Modal>
</StyledWrapper>