mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-25 05:35:41 +00:00
styling: file-picker editor component
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
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;
|
||||
}
|
||||
|
||||
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>{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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user