From 91a4c5e34f1fa4d0ef3b93c20c89539e6c2c7077 Mon Sep 17 00:00:00 2001 From: sanish-bruno Date: Thu, 1 Jan 2026 19:09:23 +0530 Subject: [PATCH] styling: file-picker editor component --- .../FilePickerEditor/StyledWrapper.js | 97 ++++++++++++++ .../src/components/FilePickerEditor/index.js | 125 +++++++++++++----- 2 files changed, 187 insertions(+), 35 deletions(-) create mode 100644 packages/bruno-app/src/components/FilePickerEditor/StyledWrapper.js diff --git a/packages/bruno-app/src/components/FilePickerEditor/StyledWrapper.js b/packages/bruno-app/src/components/FilePickerEditor/StyledWrapper.js new file mode 100644 index 000000000..ef5663fab --- /dev/null +++ b/packages/bruno-app/src/components/FilePickerEditor/StyledWrapper.js @@ -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; diff --git a/packages/bruno-app/src/components/FilePickerEditor/index.js b/packages/bruno-app/src/components/FilePickerEditor/index.js index 758a9910e..00bbb151a 100644 --- a/packages/bruno-app/src/components/FilePickerEditor/index.js +++ b/packages/bruno-app/src/components/FilePickerEditor/index.js @@ -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 ? ( -
- {!readOnly && ( - - )} - {!readOnly && <> } - { + switch (displayMode) { + case 'icon': + return ; + case 'labelAndIcon': + return ( + <> + {displayLabel} + + + ); + case 'label': + default: + return {displayLabel}; + } + }; + + // When files are selected, show file info with clear button + if (filenames.length > 0) { + return ( + +
+ + + {renderButtonText(filenames)} + + {!readOnly && ( + + )} +
+
+ ); + } + + // When no files selected, show the picker button + return ( + +
- ) : ( - + {renderButtonContent()} + + ); };