mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-27 06:34:06 +00:00
Feat/update file picker (#6614)
* styling: file-picker editor component * use filepicker component within filebody and response example filebody * edit example to use button components * fix: hide delete, disable checkbox in preview mode * make label italic * chore: change example cta buttons to filled style --------- Co-authored-by: Bijin A B <bijin@usebruno.com>
This commit is contained in:
@@ -12,6 +12,7 @@ const EditableTable = ({
|
||||
getRowError,
|
||||
showCheckbox = true,
|
||||
showDelete = true,
|
||||
disableCheckbox = false,
|
||||
checkboxLabel = '',
|
||||
checkboxKey = 'enabled',
|
||||
reorderable = false,
|
||||
@@ -288,6 +289,7 @@ const EditableTable = ({
|
||||
className="mousetrap"
|
||||
data-testid="column-checkbox"
|
||||
checked={row[checkboxKey] ?? true}
|
||||
disabled={disableCheckbox}
|
||||
onChange={(e) => handleCheckboxChange(row.uid, e.target.checked)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.file-picker-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px 8px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: color 0.15s ease;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.text} !important;
|
||||
}
|
||||
|
||||
&.read-only {
|
||||
cursor: default;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&.icon-only {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
&.icon-right {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
span {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.file-picker-selected {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
|
||||
&.read-only {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
flex-shrink: 0;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
color: ${(props) => props.theme.text};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
margin-left: 4px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: color 0.15s ease;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -2,10 +2,33 @@ import React from 'react';
|
||||
import path from 'utils/common/path';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { browseFiles } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { IconX } from '@tabler/icons';
|
||||
import { IconX, IconUpload, IconFile } from '@tabler/icons';
|
||||
import { isWindowsOS } from 'utils/common/platform';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = false, readOnly = false }) => {
|
||||
/**
|
||||
* FilePickerEditor component for selecting files
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {string|string[]} props.value - Selected file path(s)
|
||||
* @param {Function} props.onChange - Callback when file selection changes
|
||||
* @param {Object} props.collection - Collection object with pathname
|
||||
* @param {boolean} props.isSingleFilePicker - If true, only allows single file selection
|
||||
* @param {boolean} props.readOnly - If true, disables file selection
|
||||
* @param {string} props.displayMode - Display mode: 'label', 'icon', or 'labelAndIcon' (default: 'label')
|
||||
* @param {string} props.label - Custom label text (defaults to "Select File" or "Select Files")
|
||||
* @param {React.ComponentType} props.icon - Custom icon component (defaults to IconUpload)
|
||||
*/
|
||||
const FilePickerEditor = ({
|
||||
value,
|
||||
onChange,
|
||||
collection,
|
||||
isSingleFilePicker = false,
|
||||
readOnly = false,
|
||||
displayMode = 'label',
|
||||
label,
|
||||
icon: CustomIcon
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const filenames = (isSingleFilePicker ? [value] : value || [])
|
||||
.filter((v) => v != null && v != '')
|
||||
@@ -18,6 +41,8 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
|
||||
const title = filenames.map((v) => `- ${v}`).join('\n');
|
||||
|
||||
const browse = () => {
|
||||
if (readOnly) return;
|
||||
|
||||
dispatch(browseFiles([], [!isSingleFilePicker ? 'multiSelections' : '']))
|
||||
.then((filePaths) => {
|
||||
// If file is in the collection's directory, then we use relative path
|
||||
@@ -39,7 +64,8 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
|
||||
});
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
const clear = (e) => {
|
||||
e.stopPropagation();
|
||||
onChange(isSingleFilePicker ? '' : []);
|
||||
};
|
||||
|
||||
@@ -50,40 +76,69 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
|
||||
return filenames.length + ' file(s) selected';
|
||||
};
|
||||
|
||||
const buttonClass = `btn btn-secondary px-1 ${readOnly ? 'view-mode' : 'edit-mode'}`;
|
||||
const defaultLabel = isSingleFilePicker ? 'Select File' : 'Select Files';
|
||||
const displayLabel = label || defaultLabel;
|
||||
const IconComponent = CustomIcon || IconUpload;
|
||||
|
||||
return filenames.length > 0 ? (
|
||||
<div
|
||||
className={buttonClass}
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
title={title}
|
||||
>
|
||||
{!readOnly && (
|
||||
<button className="align-middle" onClick={clear} style={{ flexShrink: 0 }}>
|
||||
<IconX size={18} />
|
||||
</button>
|
||||
)}
|
||||
{!readOnly && <> </>}
|
||||
<span style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
flex: 1
|
||||
}}
|
||||
// Render the button content based on displayMode
|
||||
const renderButtonContent = () => {
|
||||
switch (displayMode) {
|
||||
case 'icon':
|
||||
return <IconComponent size={16} />;
|
||||
case 'labelAndIcon':
|
||||
return (
|
||||
<>
|
||||
<span className="label">{displayLabel}</span>
|
||||
<IconComponent size={16} />
|
||||
</>
|
||||
);
|
||||
case 'label':
|
||||
default:
|
||||
return <span>{displayLabel}</span>;
|
||||
}
|
||||
};
|
||||
|
||||
// When files are selected, show file info with clear button
|
||||
if (filenames.length > 0) {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div
|
||||
className={`file-picker-selected ${readOnly ? 'read-only' : ''}`}
|
||||
title={title}
|
||||
onClick={!readOnly ? browse : undefined}
|
||||
>
|
||||
<IconFile size={16} className="file-icon" />
|
||||
<span className="file-name">
|
||||
{renderButtonText(filenames)}
|
||||
</span>
|
||||
{!readOnly && (
|
||||
<button
|
||||
className="clear-btn"
|
||||
onClick={clear}
|
||||
title="Remove file"
|
||||
type="button"
|
||||
>
|
||||
<IconX size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
// When no files selected, show the picker button
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<button
|
||||
className={`file-picker-btn ${readOnly ? 'read-only' : ''} ${displayMode === 'icon' ? 'icon-only' : ''} ${displayMode === 'labelAndIcon' ? 'icon-right' : ''}`}
|
||||
onClick={browse}
|
||||
disabled={readOnly}
|
||||
type="button"
|
||||
title={displayLabel}
|
||||
>
|
||||
{renderButtonText(filenames)}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<button className={buttonClass} style={{ width: '100%' }} onClick={!readOnly ? browse : undefined} disabled={readOnly}>
|
||||
{isSingleFilePicker ? 'Select File' : 'Select Files'}
|
||||
</button>
|
||||
{renderButtonContent()}
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ const Wrapper = styled.div`
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
accent-color: ${(props) => props.theme.primary.solid};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ const FileBody = ({ item, collection }) => {
|
||||
'filePath'
|
||||
)}
|
||||
collection={collection}
|
||||
displayMode="labelAndIcon"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -120,6 +120,7 @@ const ResponseExampleFileBody = ({ item, collection, exampleUid, editMode = fals
|
||||
onChange={(newPath) => handleFilePathChange(row, newPath, onChange)}
|
||||
collection={collection}
|
||||
readOnly={!editMode}
|
||||
displayMode="labelAndIcon"
|
||||
/>
|
||||
)
|
||||
},
|
||||
@@ -187,6 +188,7 @@ const ResponseExampleFileBody = ({ item, collection, exampleUid, editMode = fals
|
||||
onReorder={handleParamDrag}
|
||||
showAddRow={editMode}
|
||||
showCheckbox={false}
|
||||
showDelete={editMode}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -94,6 +94,8 @@ const ResponseExampleFormUrlEncodedParams = ({ item, collection, exampleUid, edi
|
||||
reorderable={editMode}
|
||||
onReorder={handleParamDrag}
|
||||
showAddRow={editMode}
|
||||
showDelete={editMode}
|
||||
disableCheckbox={!editMode}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -139,6 +139,8 @@ const ResponseExampleHeaders = ({ editMode, item, collection, exampleUid }) => {
|
||||
reorderable={editMode}
|
||||
onReorder={handleHeaderDrag}
|
||||
showAddRow={editMode}
|
||||
showDelete={editMode}
|
||||
disableCheckbox={!editMode}
|
||||
/>
|
||||
{editMode && (
|
||||
<div className="flex justify-end mt-2">
|
||||
|
||||
@@ -265,6 +265,8 @@ const ResponseExampleMultipartFormParams = ({ item, collection, exampleUid, edit
|
||||
reorderable={editMode}
|
||||
onReorder={handleParamDrag}
|
||||
showAddRow={editMode}
|
||||
showDelete={editMode}
|
||||
disableCheckbox={!editMode}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -192,6 +192,8 @@ const ResponseExampleParams = ({ editMode, item, collection, exampleUid }) => {
|
||||
reorderable={editMode}
|
||||
onReorder={handleQueryParamDrag}
|
||||
showAddRow={editMode}
|
||||
showDelete={editMode}
|
||||
disableCheckbox={!editMode}
|
||||
/>
|
||||
{editMode && (
|
||||
<div className="flex justify-end mt-2">
|
||||
|
||||
@@ -178,6 +178,7 @@ const ResponseExampleResponseHeaders = ({ editMode, item, collection, exampleUid
|
||||
onReorder={handleHeaderDrag}
|
||||
showAddRow={editMode}
|
||||
showCheckbox={false}
|
||||
showDelete={editMode}
|
||||
/>
|
||||
{editMode && (
|
||||
<div className="flex justify-end mt-2 flex-shrink-0">
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useTheme } from 'providers/Theme';
|
||||
import TruncatedText from 'components/TruncatedText';
|
||||
import { updateResponseExampleName, updateResponseExampleDescription } from 'providers/ReduxStore/slices/collections';
|
||||
import get from 'lodash/get';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ResponseExampleTopBar = ({
|
||||
item,
|
||||
@@ -130,21 +131,22 @@ const ResponseExampleTopBar = ({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 flex-shrink-0 md:w-auto w-full md:justify-end">
|
||||
<button
|
||||
className="secondary-btn flex items-center gap-1.5 px-4 py-2 rounded-md text-xs font-medium cursor-pointer border whitespace-nowrap"
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={handleCancel}
|
||||
data-testid="response-example-cancel-btn"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="primary-btn flex items-center gap-1.5 px-4 py-2 rounded-md text-xs font-medium cursor-pointer border whitespace-nowrap"
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
style={{ padding: '6px 12px' }}
|
||||
icon={<IconDeviceFloppy size={16} />}
|
||||
onClick={handleSave}
|
||||
data-testid="response-example-save-btn"
|
||||
>
|
||||
<IconDeviceFloppy size={16} />
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -178,23 +180,23 @@ const ResponseExampleTopBar = ({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 flex-shrink-0 md:w-auto w-full md:justify-end">
|
||||
<button
|
||||
className="secondary-btn flex items-center gap-1.5 p-2 rounded-md text-xs font-medium cursor-pointer border whitespace-nowrap"
|
||||
<Button
|
||||
color="secondary"
|
||||
size="sm"
|
||||
icon={<IconCode size={16} color={theme.examples.buttonIconColor} />}
|
||||
onClick={handleGenerateCode}
|
||||
title="Generate Code"
|
||||
data-testid="response-example-generate-code-btn"
|
||||
>
|
||||
<IconCode size={16} color={theme.examples.buttonIconColor} />
|
||||
</button>
|
||||
<button
|
||||
className="secondary-btn flex items-center gap-1.5 px-4 py-2 rounded-md text-xs font-medium cursor-pointer border whitespace-nowrap"
|
||||
style={{ color: theme.examples.buttonText }}
|
||||
/>
|
||||
<Button
|
||||
color="secondary"
|
||||
size="sm"
|
||||
icon={<IconEdit size={16} color={theme.examples.buttonIconColor} />}
|
||||
onClick={onEditToggle}
|
||||
data-testid="response-example-edit-btn"
|
||||
>
|
||||
<IconEdit size={16} color={theme.examples.buttonIconColor} />
|
||||
Edit Example
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user