feat: add worker to upload CSV file#3117
feat: add worker to upload CSV file#3117icaroov wants to merge 36 commits intovtex:feat/quick-order-by-file-feature-branchfrom
Conversation
packages/core/src/components/search/SearchInput/SearchInput.tsx
Outdated
Show resolved
Hide resolved
|
The changelog needs to be added. |
|
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. |
|
Adiciona por favor na descrição do PR o arquivo .csv que estão usando no desenvolvimento |
| "react-dom": "^18.2.0" | ||
| }, | ||
| "dependencies": { | ||
| "papaparse": "^5.5.3", |
There was a problem hiding this comment.
Precisamos checar com o time da faststore a opinião sobre a adição desse pacote, consegue explicar um pouco a necessidade dele?
There was a problem hiding this comment.
Foi utilizado visando desempenho e simplicidade para processar arquivos CSV grandes. Foi indicado o uso na RFC. Ele lê o CSV em partes (streaming), não trava a tela, já usa Web Worker, detecta separador, trata aspas, dá progresso, permite cancelar e manipula arquivos grandes sem prejudicar a memória.
| 'application/vnd.ms-excel': ['.xls'], | ||
| 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': [ | ||
| '.xlsx', |
There was a problem hiding this comment.
Acredito que na RFC foi proposto o upload apenas de formato csv, surgiu a necessidade de xls e xlsx tb?
There was a problem hiding this comment.
Ajustado, aceitando apenas CSV
ArthurTriis1
left a comment
There was a problem hiding this comment.
A funcionalidade parece estar funcionando bem, mas reforço que precisamos da avaliação do time da faststore sobre a adição da lib papaparse e averiguar se devemos de fato permitir o upload de xls e xlsx.
A adição da lib do papaparse foi indicado para uso na RFC. |
|
Para desenvolver utilizamos o arquvio fornecido pela propria RFC |
|
Oie, pessoal! |
## What's the purpose of this pull request? This PR refers to the creation of the Quick Order Drawer, a new component developed for the Quick Order By File functionality. The main advantage of the Quick Order functionality is the ability to add products directly to the cart from the search, which significantly reduces the number of clicks and browsing time. <!--- Considering the context, what is the problem we'll solve? Where in VTEX's big picture our issue fits in? Write a tweet about the context and the problem itself. ---> ## How it works? <!--- Tell us the role of the new feature, or component, in its context. Provide details about what you have implemented and screenshots if applicable. ---> Creating the Quick Order Drawer layout according to Figma. <img width="1912" height="966" alt="image (3)" src="https://github.com/user-attachments/assets/65a66428-8db2-45f4-86f5-c28f4236a2e4" /> ## How to test it? <!--- Describe the steps with bullet points. Is there any external link that can be used to better test it or an example? ---> • The user uploads the file in the Quick Order interface. • After the upload is completed, the Search button becomes enabled. • The user clicks the Search button to start processing. • The system begins processing the file data. • The Dropdown enters a loading state. • Even while loading, the Dropdown can be closed without interrupting the processing. • Once processing is finished, the system automatically opens the Quick Order Drawer. ### Starters Deploy Preview <!--- Add a link to a deploy preview from `starter.store` with this branch being used. ---> <!--- Tip: You can get an installable version of this branch from the CodeSandbox generated when this PR is created. ---> ## References [Figma](https://www.figma.com/design/0c3DOaXs6rS25LyDWi7uap/Cubos-%C2%B7-FastStore-Features-(H2-2024)?node-id=1-23&node-type=canvas&t=EiFoKx4kFwgKhNEP-0) <!--- Spread the knowledge: is there any content you used to create this PR that is worth sharing? ---> <!--- Extra tip: adding references to related issues or mentioning people important to this PR may be good for the documentation and reviewing process ---> ## Checklist <em>You may erase this after checking them all 😉</em> **PR Title and Commit Messages** - [x] PR title and commit messages follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification - Available prefixes: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `ci` and `test` **PR Description** - [ ] Added a label according to the PR goal - `breaking change`, `bug`, `contributing`, `performance`, `documentation`.. **Dependencies** - [ ] Committed the `pnpm-lock.yaml` file when there were changes to the packages **Documentation** - [x] PR description - [ ] For documentation changes, ping `@Mariana-Caetano` to review and update (Or submit a doc request) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added Quick Order Drawer component enabling streamlined product ordering with header, product selection table, quantity adjustments, and checkout from a side panel. * **Documentation** * Added Storybook examples demonstrating Quick Order Drawer with multiple configuration scenarios. * **Chores** * Updated TypeScript React type definitions to latest compatible versions. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: renato <renato.neto@cubos.io> Co-authored-by: Bruna Santos <brunassdev@gmail.com> Co-authored-by: Arthur Andrade <arthurfelandrade@gmail.com>
Tasks [https://vtex-dev.atlassian.net/jira/software/projects/BE/boards/2868?selectedIssue=BE-69](https://vtex-dev.atlassian.net/jira/software/projects/BE/boards/2868?selectedIssue=BE-69) [BE-70](https://vtex-dev.atlassian.net/jira/software/projects/BE/boards/2868?selectedIssue=BE-70) Here's the completed PR description in English: - Introduced `FileUploadCard` for file selection and upload functionality, including drag-and-drop support. - Added `FileUploadStatus` to display upload progress and error handling. - Updated index files to export new components and types. - Enhanced `SearchInputField` to include an attachment button that triggers the file upload card. - Added styles for both new components to ensure proper UI integration. ## What's the purpose of this pull request? This PR introduces a new file upload feature that allows users to perform bulk searches by uploading CSV files. The feature provides a modern, user-friendly interface with drag-and-drop support, real-time upload status tracking, and comprehensive error handling to guide users through the file upload process. ## How it works? ### FileUploadCard Component - **File Selection**: Users can either click "Select file" button or drag-and-drop files into the dropzone - **Drag & Drop**: Visual feedback with border color and background changes when dragging files - **File Input**: Hidden file input that's always present in the DOM to ensure file picker works in all states - **Download Template**: Provides a CSV template download option for users who need guidance on file format - **Validation**: Only accepts CSV files (`.csv` extension) - **States**: Manages upload states (uploading, completed, error) with appropriate UI updates ### FileUploadStatus Component Displays the current status of the uploaded file with three distinct states: 1. **Uploading State** - Shows spinning CircleNotch icon - Displays "Uploading your file..." message - Shows filename 2. **Completed State** - Shows Table icon in green background - Displays filename and file size - Shows "Search" button to proceed with the uploaded file 3. **Error State** - Shows WarningOctagon icon in red background - Special styling: light pink background (#FDF6F5) with red border (#FFDFD9) - Displays error title and description in two lines - Shows two action buttons: "Download template" and "Select file" ### Error Types The component handles six different error scenarios: - **Unsupported**: Wrong file type (non-CSV files) - **Unreadable**: File can't be parsed or is corrupted - **Invalid Structure**: Missing headers or incorrect columns - **Empty**: File has no data - **Too Large**: File exceeds size limit - **Unexpected**: Generic error for unknown issues ### Integration - Added attachment icon button to `SearchInputField` component - Clicking the attachment button toggles the `FileUploadCard` visibility - Card appears as a dropdown below the search input - Smooth animations for show/hide transitions ### Key Features - **File validation**: Automatically checks file type on selection/drop - **Error recovery**: Users can easily select a new file or download template when errors occur - **Responsive design**: Components adapt to container width - **Accessibility**: Proper ARIA labels and keyboard navigation support - **Visual feedback**: Icons, colors, and animations provide clear status indication ## How to test it? ### Basic File Upload Flow 1. Navigate to a page with the search input 2. Click the attachment icon button (Paperclip icon) 3. The FileUploadCard should appear below the search input 4. Click "Select file" or drag-and-drop a CSV file 5. Observe the uploading state with spinning icon 6. After 2 seconds, see the completed state with "Search" button 7. Click the X button to remove the file and return to initial state ### Drag and Drop 1. Open the FileUploadCard 2. Drag a CSV file over the dropzone 3. Observe the visual feedback (border and background color change) 4. Drop the file to upload ### Error Handling - Unsupported File Type 1. Open the FileUploadCard 2. Select or drop a non-CSV file (e.g., .txt, .pdf, .xlsx) 3. Observe error state with red icon and pink background 4. Read error message: "Unsupported file type." / "Upload a CSV or use the template provided." 5. Click "Download template" to get a sample CSV 6. Click "Select file" to try uploading again ### File Removal 1. Upload a file successfully 2. Click the X button in the top-right of the file status 3. Component should return to initial dropzone state ### Starters Deploy Preview <!-- Add your deploy preview link here --> ## References - [Phosphor Icons](https://phosphoricons.com/) - Icon library used for UI elements - [Conventional Commits](https://www.conventionalcommits.org/) - Commit message format ## Checklist <em>You may erase this after checking them all 😉</em> **PR Title and Commit Messages** - [ ] PR title and commit messages follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification - Available prefixes: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `ci` and `test` **PR Description** - [ ] Added a label according to the PR goal - `breaking change`, `bug`, `contributing`, `performance`, `documentation`.. **Dependencies** - [ ] Committed the `pnpm-lock.yaml` file when there were changes to the packages **Documentation** - [ ] PR description [BE-70]: https://vtex-dev.atlassian.net/browse/BE-70?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * File upload card component enabling file selection via drag-and-drop or file picker with status indicators * File upload status component displaying upload progress, success, and error states with customizable error handling * Attachment button in search input for quick file upload access * **Chores** * Added component styling and Storybook demonstrations <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Bruna Santos <brunassdev@gmail.com> Co-authored-by: Arthur Andrade <arthurfelandrade@gmail.com>
d9e0250 to
87df01e
Compare
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds CSV upload and parsing with a Dropzone UI: new hooks ( Changes
Sequence DiagramsequenceDiagram
autonumber
actor User as "User"
participant SI as "SearchInput"
participant UFD as "UploadFileDropdown"
participant DZ as "Dropzone"
participant CSV as "useCSVParser"
participant WW as "WebWorker (PapaParse)"
participant FUS as "FileUploadStatus"
User->>SI: open upload UI
SI->>UFD: render dropdown
User->>DZ: drop/select CSV file
DZ->>UFD: onFilesAccepted(files)
UFD->>CSV: onParseFile(file)
CSV->>WW: parse (worker: true, chunked)
WW-->>CSV: chunk results + progress
CSV-->>UFD: resolve CSVData (rows, totals)
UFD->>FUS: display results / status
User->>FUS: click Search
FUS->>SI: emit parsed CSVData
SI->>SI: call onFileSearch(CSVData)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
87df01e to
d9e0250
Compare
d9e0250 to
5cff0a5
Compare
6a8e751 to
c197935
Compare
…resolve conflicts Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Actionable comments posted: 20
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
packages/ui/src/components/molecules/SearchInputField/styles.scss (1)
79-142:⚠️ Potential issue | 🟡 MinorDuplicate rule blocks — lines 79–105 are repeated verbatim at lines 115–142.
Three selectors are defined twice within
[data-fs-search-input-field]:
Selector First occurrence Duplicate [data-fs-icon-button][data-fs-button-variant]79–87 115–124 [data-fs-search-input-field-attachment-button]89–97 126–134 [data-fs-search-input-field-separator]99–105 136–142 This looks like a rebase/merge artifact. Remove one set to avoid confusion and potential specificity surprises.
Proposed fix — remove duplicate blocks
- [data-fs-icon-button][data-fs-button-variant] { - `@include` media(">=notebook") { - min-height: var(--fs-search-input-field-button-min-height); - padding-top: var(--fs-search-input-field-button-padding-top-desktop); - padding-bottom: - var( - --fs-search-input-field-button-padding-bottom-desktop - ); - } - } - - [data-fs-search-input-field-attachment-button] { - [data-fs-icon] { - transform: rotate(90deg); - } - - &:hover { - --fs-button-border-radius: var(--fs-border-radius-circle); - } - } - - [data-fs-search-input-field-separator] { - display: inline-block; - width: 1px; - height: var(--fs-spacing-3); - margin: 0 var(--fs-spacing-0); - background-color: var(--fs-border-color-light, `#e0e0e0`); - }packages/components/src/molecules/FileUploadCard/FileUploadCard.tsx (2)
256-269:⚠️ Potential issue | 🔴 CriticalDefault CSV template is malformed — column/row structure is broken.
The template string
'SKU,Quantity\nAB001,AB100,AB999\n2,5,49'has a 2-column header but 3-value data rows, and the data appears transposed (SKUs on one row, quantities on the next). Downloading this template will confuse users and fail the parser's own validation.Proposed fix
- const csvContent = 'SKU,Quantity\nAB001,AB100,AB999\n2,5,49' + const csvContent = 'SKU,Quantity\nAB001,2\nAB100,5\nAB999,49'
165-169:⚠️ Potential issue | 🟠 Major
isValidFileTypeignores theacceptprop — hardcoded to.csvonly.The component accepts an
acceptprop (defaulting to'.csv'), but validation on lines 165-169 is hardcoded to['.csv']. If a consumer passesaccept=".csv,.xlsx", the input will allow.xlsxselection but validation will reject it.Proposed fix: derive valid extensions from the accept prop
- const isValidFileType = (file: File): boolean => { - const fileName = file.name.toLowerCase() - const validExtensions = ['.csv'] - return validExtensions.some((ext) => fileName.endsWith(ext)) - } + const isValidFileType = (file: File): boolean => { + const fileName = file.name.toLowerCase() + const validExtensions = accept.split(',').map((ext) => ext.trim().toLowerCase()) + return validExtensions.some((ext) => fileName.endsWith(ext)) + }packages/core/src/components/search/SearchInput/SearchInput.tsx (1)
319-333:⚠️ Potential issue | 🟠 MajorProps spread overrides all defaults instead of merging.
...(fileUploadCardProps ?? DEFAULT_FILE_UPLOAD_CARD_PROPS)uses an all-or-nothing fallback. If a consumer passesfileUploadCardProps={{ title: 'Custom' }}, all other defaults (errorMessages,getCompletedStatusText, etc.) are lost.Spread defaults first, then overrides:
🔧 Suggested fix
- ...(fileUploadCardProps ?? DEFAULT_FILE_UPLOAD_CARD_PROPS), + ...DEFAULT_FILE_UPLOAD_CARD_PROPS, + ...fileUploadCardProps,
🤖 Fix all issues with AI agents
In `@packages/components/package.json`:
- Around line 37-39: Remove the unused dependency entry "@koale/useworker" from
packages/components/package.json and regenerate the lockfile (npm/yarn/pnpm
install) so the package is actually removed from the install tree; confirm there
are no imports of "@koale/useworker" by searching the repo and run the build to
ensure tree-shaking still preserves consumers who don't use CSV/upload, and keep
"papaparse" and "react-dropzone" as they are used by useCSVParser.ts and
Dropzone.tsx respectively.
In `@packages/components/src/hooks/useCSVParser.ts`:
- Line 38: The default parameter options: CSVParserOptions = {} creates a new
object each render and invalidates the useCallback onParseFile; change the
signature of useCSVParser to accept options without a fresh-object default
(e.g., no default or options?: CSVParserOptions) and inside the hook create a
stable mergedOptions using a stable reference (either a top-level defaultOptions
constant or useMemo(() => ({ ...defaultOptions, ...options }), [options])) and
then use mergedOptions in the dependency array for onParseFile so the callback
is not recreated every render.
- Around line 126-135: The findColumnIndex function currently uses
header.toLowerCase().trim().includes(name.toLowerCase()) which can yield false
positives (e.g., "description" matching "id"); change the matching logic in
findColumnIndex to use either exact token equality or a case-insensitive
word-boundary regex: normalize header and name (trim, toLowerCase()), split
header into tokens or apply new RegExp(`\\b${escapeRegex(name)}\\b`, 'i') and
test against the header, falling back to exact equality rather than substring
includes; ensure you update references to headers and columnNames accordingly
and keep the comparison case-insensitive.
- Around line 225-228: The code mutates PapaParse's chunk object by assigning
results.data = results.data.slice(1); instead, create a local variable (e.g.,
const rows = results.data.slice(1)) and use that local variable for further
processing, remove the in-place assignment to results.data and stop mutating the
parser's result object; update subsequent uses (within the parse chunk handling
in useCSVParser / the header-processing branch where isHeaderProcessed is set)
to reference the new local variable and tighten types to avoid any.
In `@packages/components/src/hooks/useFileUpload.ts`:
- Around line 32-45: In useFileUpload's switch on `code`, wrap the `case
'file-too-large':` body in braces to scope `const sizeMB` (preventing the Biome
lint leak), and update the 'file-invalid-type' message to match actual allowed
types by referencing `DEFAULT_FILE_UPLOAD_OPTIONS.acceptedTypes` (or explicitly
state "CSV only" if that constant only contains CSV); keep the `too-many-files`
message using `options.maxFiles` as-is. Ensure you modify the switch in
useFileUpload to use a braced block for the sizeMB calculation and change the
invalid-type string to the correct accepted-types wording.
- Line 27: The hook useFileUpload currently replaces defaults when called with a
partial options object, causing fields like maxSize and maxFiles to become
undefined; fix this by shallow-merging options with DEFAULT_FILE_UPLOAD_OPTIONS
(e.g., const resolvedOptions = { ...DEFAULT_FILE_UPLOAD_OPTIONS, ...options })
and use resolvedOptions throughout the function (including in the error message
logic where Math.round(...) is used) so maxSize/maxFiles never become undefined,
and update any useCallback/useMemo dependency arrays to reference
resolvedOptions (or its individual props) instead of the raw options object.
In `@packages/components/src/index.ts`:
- Around line 3-7: The package root currently re-exports CSV parser types but
not FileUploadOptions or DEFAULT_FILE_UPLOAD_OPTIONS; update the exports so
consumers can import the file-upload types/constants from the package root by
adding exports for FileUploadOptions and DEFAULT_FILE_UPLOAD_OPTIONS from
'./hooks/useFileUpload' (alongside the existing useFileUpload re-export in
hooks/index.ts) so that FileUploadOptions and DEFAULT_FILE_UPLOAD_OPTIONS are
available at the package level.
In `@packages/components/src/molecules/Dropzone/Dropzone.tsx`:
- Around line 55-66: Update the JSDoc for the Dropzone props to accurately
describe their behavior: change the comment for noClick to indicate it disables
opening the file dialog on click, change noKeyboard to indicate it disables
opening the file dialog via keyboard, and change noDrag to indicate it disables
drag-and-drop on the drop zone; update these comments next to the noClick,
noKeyboard, and noDrag prop definitions in the Dropzone component so public API
docs reflect the correct semantics.
- Around line 141-151: The component currently spreads {...getRootProps()}
before {...otherProps}, which lets consumer props override critical handlers; in
the Dropzone component swap the spreads so consumer props are applied first and
then {...getRootProps()} is spread last (keep ref, data-attributes and testId in
place) to ensure getRootProps() handlers (onClick, onKeyDown, onDragEnter, etc.)
cannot be silently overridden by otherProps.
In `@packages/components/src/molecules/FileUploadCard/FileUploadCard.tsx`:
- Around line 131-134: The FileUploadCard props currently destructure
formatterFileSize and hasError but never use them; remove these dead props from
the public API by deleting formatterFileSize and hasError from the props
destructuring and from the component's Props/interface/type definition (and any
default values), keeping only formatterFileName and isUploading (and other used
props), or alternatively, if you intend to preserve behavior, implement their
logic inside FileUploadCard where formatterFileSize should be used to format
displayed size and hasError should drive error UI; prefer removing the unused
props to avoid breaking the public surface if no behavior is required.
In `@packages/core/src/components/search/fileUploadCardDefaults.ts`:
- Around line 15-42: Import the FileUploadErrorType enum and add a type
annotation to the errorMessages export to ensure keys align with the enum (e.g.,
type errorMessages as Record<FileUploadErrorType, { title: string; description:
string }>), then update the declaration of errorMessages in
fileUploadCardDefaults.ts so TypeScript will flag any missing or mismatched
keys; keep the existing object values but ensure the type is applied to the
errorMessages symbol.
In `@packages/core/src/components/search/SearchInput/SearchInput.tsx`:
- Around line 216-237: In handleSearch, remove the two Portuguese console.log
debug statements that print when csvData is null and after building payload;
either delete them or replace with a properly gated debug/logger call (e.g., use
an environment check or a centralized logger) so no informal/Portuguese console
output ships to production; locate handleSearch in SearchInput.tsx and update
the code around the payload creation and window.dispatchEvent call accordingly.
- Line 132: The state variable isUploadModalOpen is never set to true so
UploadFileDropdown never mounts; either wire setIsUploadModalOpen(true) to the
user action that should open the upload UI (for example call
setIsUploadModalOpen(true) from the attachment button's onClick/handler
currently rendering the attachment icon) ensuring the dynamically imported
UploadFileDropdown (imported in lines around the dynamic import) and the
conditional JSX that checks isUploadModalOpen are reachable, or remove the
dynamic import and the conditional render block for UploadFileDropdown to
eliminate dead code; update any handler names you use (e.g., the attachment
button click handler) and ensure you still call setIsUploadModalOpen(false) to
close the modal.
- Around line 320-332: Remove the unsafe type assertion "as FileUploadCardProps"
on the FileUploadCard props spread and let TypeScript validate the prop object;
update the constructed props (isOpen, onDismiss/handleDismiss,
onFileSelect/handleFileSelect, onDownloadTemplate/handleDownloadTemplate,
formatterFileSize/formatFileSize, formatterFileName/formatFileName,
onSearch/handleSearch, isUploading/isCsvProcessing, hasError/!!csvError and the
spread of fileUploadCardProps ?? DEFAULT_FILE_UPLOAD_CARD_PROPS) so they exactly
match the FileUploadCardProps shape (add any missing required props, correct
types, or adjust DEFAULT_FILE_UPLOAD_CARD_PROPS), fix any resulting type errors
in those helpers or default props, and re-run the type checker to ensure no
assertion is needed.
In
`@packages/core/src/components/search/UploadFileDropdown/UploadFileDropdown.tsx`:
- Around line 141-146: handleUseData currently just logs parsed CSV rows
(csvData.data) and must not leak data; replace the TODO/console.log by invoking
a prop callback or actual processing: add an optional prop like onUseData
(signature: (data: ParsedCsvType) => void) to the UploadFileDropdown component
and call onUseData(csvData.data) inside handleUseData (guarding existence), or
implement the real handling logic here if this component should process/upload
the data itself; remove the console.log and TODO placeholder and ensure
handleUseData returns/updates state or triggers the callback accordingly.
- Around line 47-64: The component UploadFileDropdown currently shows the same
CSV parsing error twice: once via the useEffect that calls pushToast when
csvError is set and again via the inline error JSX that renders csvError; remove
the duplicate by deleting (or conditionally hiding) the inline error block that
directly renders csvError (the JSX that displays the inline banner) so only the
toast produced in the useEffect remains, or alternatively remove the useEffect
pushToast and keep the inline banner—ensure you only keep one presentation path
for csvError and update references to csvError accordingly.
In `@packages/core/src/utils/utilities.ts`:
- Around line 148-156: formatFileSize currently produces NaN for negative inputs
and undefined for sizes >= 1 TB; update the formatFileSize function to (1) guard
against negative bytes by normalizing negative inputs to 0 or returning "0
Bytes" for bytes <= 0, (2) extend the sizes array to include larger units (e.g.,
'TB', 'PB', ...) or clamp the computed index i to the last available unit, and
(3) ensure i is bounded (Math.max(0, Math.min(i, sizes.length - 1))) before
indexing sizes to avoid undefined; modify the function body (formatFileSize,
sizes array, and the computation of i) accordingly.
In `@packages/ui/package.json`:
- Line 57: The peerDependency entry "@faststore/components": "workspace:*" in
packages/ui/package.json pins the published package to an exact dev version;
update the peerDependencies entry to use "workspace:^" (i.e.,
"@faststore/components": "workspace:^") so the published package resolves to a
caret range (e.g., ^3.97.0-dev.2) and allows compatible minor/patch updates for
consumers.
In `@packages/ui/src/components/molecules/Dropzone/styles.scss`:
- Around line 69-78: The selector &[data-fs-dropzone-disabled="true"] currently
sets pointer-events: none which prevents the cursor: not-allowed and nested
&:hover from ever taking effect; remove the pointer-events: none declaration
from that rule so the not-allowed cursor and &:hover background/border styles
are visible, and ensure disabling is enforced elsewhere (e.g., using
aria-disabled and JS guards in the Dropzone component's event handlers) rather
than via CSS pointer suppression; keep the cursor: not-allowed and &:hover
blocks under &[data-fs-dropzone-disabled="true"] and update the Dropzone logic
to ignore pointer interactions when disabled.
In `@packages/ui/src/components/molecules/SearchInputField/styles.scss`:
- Around line 68-72: The transition for box-shadow in the SearchInputField
styles uses the timing variable in both the duration and the timing-function
slots; update the transition declaration so box-shadow uses
var(--fs-search-input-field-transition-timing) for the duration and
var(--fs-search-input-field-transition-function) for the timing function (i.e.,
replace the second var used for box-shadow with
--fs-search-input-field-transition-function) to match the border transition and
produce a valid transition.
🧹 Nitpick comments (21)
packages/ui/src/components/molecules/SearchInputField/styles.scss (2)
104-104: Hardcoded fallback color#e0e0e0.Consider extracting this into a design token (e.g.,
--fs-search-input-field-separator-color) so it stays in sync with the rest of the theming system rather than relying on a magic hex value.
107-113: Upload button uses a fixedrightoffset — fragile coupling.The
right: 2.5rem/right: 3remvalues are magic numbers tightly coupled to the sibling button widths. If the search button or attachment button sizes change, this will break. Consider using flexbox ordering or logical positioning within the actions container instead of absolute offsets.packages/ui/src/styles/components.scss (1)
67-67: Dropzone import breaks alphabetical ordering of Molecules section.All other molecule imports are sorted alphabetically.
Dropzoneshould be placed betweenDropdown(line 31) andFileUploadCard(line 32).Suggested fix
`@import` "../components/molecules/Dropdown/styles"; +@import "../components/molecules/Dropzone/styles"; `@import` "../components/molecules/FileUploadCard/styles";And remove line 67.
packages/ui/src/components/molecules/FileUploadCard/styles.scss (1)
18-19: Hardcoded hex colors should use design tokens for consistency and theming.Lines 18-19 introduce
#0366ddand#bfdbfeas raw hex values. Similar hardcoded colors appear throughout this file (#ffffffat line 160,#f0f7ffat line 105,#d6d6d6at line 193,#f5f5f5at line 230). The rest of the design system usesvar(--fs-*)tokens. These should follow the same pattern to support theming and dark mode.packages/ui/src/components/molecules/Dropzone/styles.scss (2)
7-7: Misleading CSS custom property name--fs-modal-bg-color.This token is named
modalbut scoped to the Dropzone component. Consider renaming to--fs-dropzone-bg-colorfor clarity and to avoid confusion with any actual modal tokens.
25-25: Prefer explicit transition properties overtransition: all.
transition: allcan cause unintended transitions on properties you didn't mean to animate and has a minor performance cost. Specify only the properties that should transition (e.g.,background-color, border-color, transform).packages/core/src/components/search/UploadFileDropdown/section.module.scss (1)
77-92: Duplicated magic number500pxformax-width.Both
[data-fs-upload-result-file-name]and[data-fs-upload-result-file-rows]share the samemax-width: 500pxalong with identical overflow/ellipsis styles. Consider extracting to a local CSS variable (e.g.,--fs-upload-result-max-width: 500px) or a shared mixin.packages/components/src/molecules/FileUploadStatus/FileUploadStatus.tsx (2)
180-194:getErrorMessage()is called twice, creating redundant objects.On lines 183 and 186,
getErrorMessage()is invoked separately fortitleanddescription. Destructure once before rendering:♻️ Proposed fix
{state === FileUploadState.Error ? ( - <> - <p data-fs-file-upload-status-text-error> - {getErrorMessage().title} - </p> - <p data-fs-file-upload-status-text-error> - {getErrorMessage().description} - </p> - </> + (() => { + const { title, description } = getErrorMessage() + return ( + <> + <p data-fs-file-upload-status-text-error>{title}</p> + <p data-fs-file-upload-status-text-error>{description}</p> + </> + ) + })()Alternatively, compute it outside the JSX at the top of the component body:
const errorInfo = state === FileUploadState.Error ? getErrorMessage() : nullThen reference
errorInfo.title/errorInfo.descriptionin JSX.
166-172: Consider adding a live region for accessibility.Upload status transitions (uploading → completed, or → error) won't be announced to screen readers. Adding
role="status"(for non-urgent updates) orrole="alert"(for errors) andaria-live="polite"on the container would improve the experience for assistive technology users.♿ Proposed enhancement
<div data-fs-file-upload-status data-fs-file-upload-status-state={state} data-testid={testId} + role={state === FileUploadState.Error ? 'alert' : 'status'} + aria-live="polite" {...otherProps} >packages/ui/src/components/molecules/FileUploadStatus/styles.scss (2)
13-15: Hardcoded hex colors break the design token system.Multiple hex values are used instead of design tokens:
#08a822,#d31a15(lines 13-15),#fdf6f5/#ffdfd9(lines 47-48),#0366dd/#0252b8(lines 197-199, 219-225), and#d6d6d6(line 224). This undermines theme consistency and dark-mode support. Replace with appropriate--fs-color-*tokens or define new component-level tokens at the top of the block.Also applies to: 47-48, 197-199, 219-225
29-29: Remove commented-out code.
// height: 188px;is dead code. Remove it to keep the stylesheet clean.packages/core/src/components/search/fileUploadCardDefaults.ts (1)
13-14:getCompletedStatusTextalways formats as KB.For files larger than ~1 MB, displaying
1024.0 KBinstead of1.0 MBis awkward. Consider a simple size formatter that adapts the unit. This is minor since CSV files for quick order are likely small.packages/core/src/components/sections/Navbar/Navbar.tsx (2)
116-120: Shallow fallback — partial CMS overrides won't merge with defaults.The nullish coalescing (
??) provides an all-or-nothing fallback. If CMS supplies a partialfileUploadCardProps(e.g., onlytitle), the defaults won't fill in the missing fields — downstream components will receiveundefinedfor the omitted labels.Since this is temporary (per the TODO in
fileUploadCardDefaults.ts), this is fine for now, but when CMS integration lands, consider a shallow merge:♻️ Future-proof merge approach
searchInput={{ ...searchInput, - fileUploadCardProps: - searchInput.fileUploadCardProps ?? DEFAULT_FILE_UPLOAD_CARD_PROPS, + fileUploadCardProps: { + ...DEFAULT_FILE_UPLOAD_CARD_PROPS, + ...searchInput.fileUploadCardProps, + }, }}This ensures any CMS-provided fields override the defaults while missing fields retain safe fallback values.
44-58:fileUploadCardPropstype usesPartial<Record<string, ...>>forerrorMessagesbut downstream expectsFileUploadErrorTypekeys.The
errorMessagesfield is typed asPartial<Record<string, { title: string; description: string }>>(line 55-57) — this accepts arbitrary string keys. Downstream,FileUploadStatusProps.errorMessagesexpectsPartial<Record<FileUploadErrorType, ...>>. Consider aligning the type here withFileUploadErrorTypefor compile-time safety.packages/components/src/molecules/Dropzone/Dropzone.tsx (1)
167-189: Hardcoded accepted/rejected file lists look like debug UI.The raw
file.name - file.size bytesand error lists are rendered unconditionally inside the dropzone. This doesn't seem intended for production — consumers would typically handle display themselves via callbacks. Consider removing these or making them opt-in via a prop (e.g.,showFileList).packages/components/src/molecules/FileUploadCard/FileUploadCard.tsx (1)
171-200:handleFileChangeandhandleDropduplicate nearly identical validation/upload logic.Both handlers perform the same sequence: set file → validate type → set upload state → optionally simulate → call
onFileSelect. Extract a shared helper to reduce the duplication and the risk of the two paths diverging.Also applies to: 213-246
packages/components/src/hooks/useCSVParser.ts (1)
168-171: Silent error pruning drops diagnostic data.When errors exceed 1000,
errors.splice(0, 500)discards the oldest 500 without any indication. A user parsing a large malformed file would lose the initial (most useful) error context. Consider keeping the first N errors and discarding later ones instead.Proposed fix
// Limit errors to avoid excessive memory usage - if (errors.length > 1000) { - errors.splice(0, 500) - } + if (errors.length >= 1000) { + return null // Stop collecting, first errors are most useful + }packages/core/src/components/search/UploadFileDropdown/UploadFileDropdown.tsx (2)
24-231: Significant logic duplication withSearchInput.tsx.This component reimplements CSV parsing, template download, and error handling that
SearchInput.tsxalso sets up (lines 148–237 there). Both calluseCSVParser, build a blob for template download, and managecsvDatastate independently. Consider extracting the shared CSV workflow into a single custom hook (e.g.,useCSVUploadFlow) to keep the logic DRY and avoid divergence.
148-231: No props = zero configurability and no i18n support.All user-facing strings ("Search", "Drop a file to search in bulk", "Download Template", "products found", etc.) are hard-coded.
SearchInput.tsxalready defines afileUploadCardPropsshape for CMS-editable copy and usesDEFAULT_FILE_UPLOAD_CARD_PROPS. This component should follow the same pattern, accepting text props so copy can be localized or edited from CMS.packages/core/src/components/search/SearchInput/SearchInput.tsx (2)
130-142: Five overlapping boolean/state variables manage upload visibility — simplify.
isUploadModalOpen,fileUploadVisible,isUploadOpen,hasFile, pluscsvDataall partially control what's shown. This makes the state machine hard to reason about and prone to inconsistencies (e.g.,isUploadOpen || hasFile || fileUploadVisibleon line 322).Consider consolidating into a single discriminated state, e.g.:
type UploadUIState = 'closed' | 'dropzone' | 'processing' | 'results' const [uploadState, setUploadState] = useState<UploadUIState>('closed')This would eliminate the combinatorial explosion and make transitions explicit.
48-55: Dynamic import resolves.defaultbut the barrel already re-exports default.The
.then((mod) => mod.default)on line 53 is necessary here becausenext/dynamicexpects a component, but note that the barrelindex.tsdoesexport { default }. This works, but if the barrel ever switches to a named export, this will silently break at runtime. A named export in the barrel + named import indynamicwould be more resilient.
packages/core/src/components/search/UploadFileDropdown/UploadFileDropdown.tsx
Show resolved
Hide resolved
packages/ui/src/components/molecules/SearchInputField/styles.scss
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
packages/components/src/molecules/SearchInputField/SearchInputField.tsx (1)
123-126:⚠️ Potential issue | 🟡 MinorBoth
aria-labelprops on icon buttons are optional with no fallback.
attachmentButtonAriaLabelandsubmitButtonAriaLabelcan beundefined, which would render<IconButton>elements without accessible names — a WCAG violation. Consider providing sensible defaults (e.g.,"Attach file"and"Submit search").This predates this PR but is now codified in the public interface, making it a good time to address it.
Also applies to: 136-139
packages/components/src/molecules/FileUploadCard/FileUploadCard.tsx (2)
256-269:⚠️ Potential issue | 🔴 CriticalDefault CSV template content is malformed.
Line 261: The header declares 2 columns (
SKU,Quantity) but each "data row" has 3 comma-separated values. This produces a broken CSV that will confuse users and likely fail validation when re-uploaded.Additionally, the data layout seems transposed — it looks like columns were written as rows. A corrected version might be:
Proposed fix (assuming 2-column SKU+Quantity template)
- const csvContent = 'SKU,Quantity\nAB001,AB100,AB999\n2,5,49' + const csvContent = 'SKU,Quantity\nAB001,2\nAB100,5\nAB999,49'
165-169:⚠️ Potential issue | 🟠 Major
isValidFileTypehardcodes.csvand ignores theacceptprop.The
acceptprop defaults to'.csv'but can be overridden by consumers. Yet validation always checks only['.csv'], so passingaccept=".xlsx"would accept the file in the native picker but then immediately reject it in your validation logic.Derive the valid extensions from the
acceptprop instead:Proposed fix
const isValidFileType = (file: File): boolean => { const fileName = file.name.toLowerCase() - const validExtensions = ['.csv'] + const validExtensions = accept + .split(',') + .map((ext) => ext.trim().toLowerCase()) + .filter((ext) => ext.startsWith('.')) return validExtensions.some((ext) => fileName.endsWith(ext)) }packages/core/src/components/search/SearchInput/SearchInput.tsx (1)
319-334:⚠️ Potential issue | 🔴 CriticalDefault props are replaced, not merged — partial
fileUploadCardPropsdrops all other defaults.
...(fileUploadCardProps ?? DEFAULT_FILE_UPLOAD_CARD_PROPS)uses nullish coalescing: iffileUploadCardPropsis provided (even with a single field like{ title: 'Custom' }), the entireDEFAULT_FILE_UPLOAD_CARD_PROPSis skipped. Required props likefileInputAriaLabel,dropzoneAriaLabel,errorMessages, etc. will beundefined, likely causing runtime errors or broken UI.Merge defaults first, then override with user-provided props.
Suggested fix
{...({ isOpen: isUploadOpen || hasFile || fileUploadVisible, onDismiss: handleDismiss, onFileSelect: handleFileSelect, onDownloadTemplate: handleDownloadTemplate, formatterFileSize: formatFileSize, formatterFileName: formatFileName, onSearch: handleSearch, isUploading: isCsvProcessing, hasError: !!csvError, - ...(fileUploadCardProps ?? DEFAULT_FILE_UPLOAD_CARD_PROPS), + ...DEFAULT_FILE_UPLOAD_CARD_PROPS, + ...fileUploadCardProps, } as FileUploadCardProps)}
🤖 Fix all issues with AI agents
In `@packages/components/src/molecules/Dropzone/Dropzone.tsx`:
- Around line 141-191: The Dropzone root <div> in the Dropzone component is
missing an accessible role/label; update the component (Dropzone) to accept an
ariaLabel prop (or derive a default) and apply a suitable role (e.g.,
role="button" or role="region") and aria-label on the root element that uses
ref/getRootProps (the element with data-fs-dropzone and {...getRootProps()});
ensure the ariaLabel prop is passed through otherProps or merged into
getRootProps so screen readers can identify the dropzone, and only add the
role/aria-label when not disabled to avoid misleading semantics.
- Around line 141-152: The forwarded ref passed into the Dropzone component is
being overwritten by the ref returned inside getRootProps() because
{...getRootProps()} is spread after ref={ref}; update the Dropzone to merge the
consumer ref and the internal ref from getRootProps() (e.g., via a mergeRefs or
useMergeRefs utility) and pass that merged callback as the effective ref on the
root div so both the component internals and the forwarded ref are honored;
reference the Dropzone component, the getRootProps() call, and the ref prop when
applying the merged ref.
In `@packages/core/CHANGELOG.md`:
- Around line 6-12: The changelog entries for useCSVParser, useFileUpload, and
FileUploadCard are recorded under the wrong package; update the CHANGELOG so
these items appear in the components package changelog (or explicitly note in
core that these are re-exports). Move the three bullet points (useCSVParser,
useFileUpload, FileUploadCard) out of the core changelog and add them to the
components changelog, or if core truly re-exports them, keep concise entries in
core that reference the components changelog and mention the re-export; ensure
the symbols useCSVParser, useFileUpload, and FileUploadCard are mentioned
exactly as in the diff for discoverability.
In `@packages/core/src/components/search/SearchInput/SearchInput.tsx`:
- Around line 150-159: The inline options object passed to useCSVParser ({
delimiter: ',', skipEmptyLines: true }) creates a new reference each render
which invalidates onParseFile; fix this by extracting the options into a stable
reference — either declare a module-level constant (e.g. CSV_PARSER_OPTIONS) or
wrap the object in useMemo and pass that memoized variable to useCSVParser,
ensuring useCSVParser(...) receives a stable options reference so onParseFile
(and related callbacks) remain memoized.
- Around line 210-214: handleDismiss currently clears csvData, hides the upload
UI and calls onClearError but fails to reset the upload-state flags (hasFile and
isUploadOpen), causing isOpen to remain true on reopen; update handleDismiss to
also call setHasFile(false) and setIsUploadOpen(false) (in addition to
setCsvData(null), setFileUploadVisible(false), and onClearError()) so the upload
card opens in a clean initial state.
In `@packages/core/src/components/search/UploadFileDropdown/section.module.scss`:
- Around line 1-2: The `@import` is nested inside the .section selector and uses a
.scss extension, which triggers Stylelint errors; move the import statement
(currently "@import
\"@faststore/ui/src/components/molecules/Dropzone/styles.scss\";") to the
top-level of the module (before any selectors) and drop the .scss extension so
it reads `@import` "@faststore/ui/src/components/molecules/Dropzone/styles"; if
you intentionally needed those Dropzone rules scoped under .section instead,
replace the import approach by using `@use` with a namespace and then reference
the tokens, or copy/override only the required styles inside the .section block
to preserve scoping.
In
`@packages/core/src/components/search/UploadFileDropdown/UploadFileDropdown.tsx`:
- Around line 111-139: handleDownloadTemplate currently calls a.click() on a
detached anchor (which can fail in some browsers like Firefox) and relies on the
try/catch even though onGenerateTemplate swallows errors and returns null;
update handleDownloadTemplate to explicitly check for a falsy csvContent and
show the error toast in that branch, and when creating the anchor element append
it to document.body before calling a.click(), then revokeObjectURL and remove
the anchor after the download; reference the handleDownloadTemplate function,
the onGenerateTemplate call, and pushToast for where to show the error toast.
- Around line 35-40: The inline options object passed to useCSVParser
(containing delimiter, skipEmptyLines, skuColumnNames, quantityColumnNames)
creates a new reference each render which makes the hook's onParseFile unstable;
fix this by hoisting that options object to a module-level constant or memoizing
it with useMemo (e.g., const csvOptions = useMemo(() => ({ delimiter: ',',
skipEmptyLines: true, skuColumnNames: [...], quantityColumnNames: [...] }), []))
and pass csvOptions to useCSVParser so onParseFile becomes stable.
In `@packages/core/src/utils/utilities.ts`:
- Around line 120-139: The current formatFileName logic early-returns based on
total fileName length but only truncates nameWithoutExtension, which allows long
extensions to exceed maxLength; update formatFileName to ensure the final
returned string does not exceed maxLength by accounting for extension length:
compute availableBase = maxLength - extension.length, if availableBase <= 0
return a truncated extension-aware placeholder (e.g., slice from the end of the
extension), otherwise truncate nameWithoutExtension to availableBase using the
existing start/.../end approach (use maxLength, extensionIndex, extension,
nameWithoutExtension variables) and then return
`${truncatedNameWithoutExtension}${extension}` so the overall length is capped
at maxLength.
In `@packages/ui/src/components/molecules/Dropzone/styles.scss`:
- Around line 178-188: Replace the deprecated CSS property `clip` on the file
input selector (`input[type="file"]`) with a modern `clip-path` value: remove
`clip: rect(0, 0, 0, 0);` and add a `clip-path: inset(50%);` (or `clip-path:
inset(0 0 0 0 round 0)` if you prefer explicit zero inset) to achieve the same
visual hiding; keep the existing positioning, sizing, overflow and
accessibility-related rules intact so `input[type="file"]` remains visually
hidden but focusable.
- Around line 192-207: Replace the old max-width media feature with modern range
notation: change the media query declaration from "@media (max-width: 768px)" to
the range syntax "@media (width <= 768px)" and keep the inner block for
[data-fs-dropzone], [data-fs-dropzone-icon] svg, and [data-fs-dropzone-text]
unchanged so the same styles apply under the new media query.
In `@packages/ui/src/components/molecules/SearchInputField/styles.scss`:
- Around line 107-113: The upload button selector (&[data-fs-search-input-field]
[data-fs-search-input-field-upload-button]) uses the physical property "right"
which breaks RTL; change both occurrences (base and inside the `@include`
media(">=notebook") block) to the logical property "inset-inline-end" so
positioning respects text direction and matches the other rules in this
stylesheet.
- Around line 79-105: Remove the duplicated CSS block that re-declares the
selectors [data-fs-icon-button][data-fs-button-variant],
[data-fs-search-input-field-attachment-button] (and its nested [data-fs-icon] +
&:hover rule) and [data-fs-search-input-field-separator] — keep only the
existing definitions (the later set) to avoid emitting duplicate styles; delete
the earlier duplicate block that includes the media query adjustments,
attachment button transform/hover, and separator styles so only one set of those
selectors remains.
🧹 Nitpick comments (11)
packages/ui/src/components/molecules/SearchInputField/styles.scss (1)
99-105: Consider using a design token instead of hardcoded#e0e0e0fallback.The separator's
background-colorfalls back to a hardcoded hex value. If this color is used elsewhere or if the theme changes, it won't update. If a suitable token exists (or can be added), prefer that for consistency.packages/ui/src/components/molecules/FileUploadCard/styles.scss (1)
13-13: Consider replacing hardcoded hex colors with design tokens.Multiple hex values (
#0366dd,#0252b8,#d6d6d6,#bfdbfe,#ffffff) are scattered throughout this file andFileUploadStatus/styles.scss. Extracting these into CSS custom properties (design tokens) at the component root would improve theming consistency and maintainability. Not a blocker — can be addressed in a follow-up.Also applies to: 18-18, 19-19, 160-160, 188-188, 189-189, 191-191, 192-192, 193-193, 194-194, 195-195, 207-207
packages/components/src/index.ts (1)
431-432: Dropzone export is placed after the Organisms section.All other molecule exports live between the
// Moleculesand// Organismscomments. This placement at the very end breaks the established file organization.Suggested move
Move these two lines into the Molecules section (e.g., after the existing
Dropdownexports around line 107, alphabetically).packages/ui/src/components/molecules/Dropzone/styles.scss (1)
7-7: Design token--fs-modal-bg-colorappears copy-pasted from Modal.This variable name is misleading in a Dropzone context. Consider renaming to
--fs-dropzone-bg-colorfor clarity and to avoid accidental coupling with Modal tokens.packages/components/src/molecules/FileUploadStatus/FileUploadStatus.tsx (1)
180-188:getErrorMessage()is called twice per render in error state.Each call re-computes the same lookup. Store the result once:
Proposed fix
{state === FileUploadState.Error ? ( - <> - <p data-fs-file-upload-status-text-error> - {getErrorMessage().title} - </p> - <p data-fs-file-upload-status-text-error> - {getErrorMessage().description} - </p> - </> + (() => { + const { title, description } = getErrorMessage() + return ( + <> + <p data-fs-file-upload-status-text-error>{title}</p> + <p data-fs-file-upload-status-text-error>{description}</p> + </> + ) + })()Or extract to a variable before the return statement.
packages/components/src/molecules/FileUploadCard/FileUploadCard.tsx (1)
171-199: Duplicated file handling logic betweenhandleFileChangeandhandleDrop.The validation → set state → simulate upload → callback sequence is nearly identical in both handlers. Extract a shared
processFile(files: File[])helper to reduce duplication and ensure future changes apply consistently.Also applies to: 213-246
packages/components/src/hooks/useCSVParser.ts (1)
168-171: Error truncation discards the earliest diagnostics.When errors exceed 1000,
errors.splice(0, 500)drops the first 500 entries — the rows most likely near the file header. A simple length cap (stop pushing after N) would be more predictable and avoids repeated O(n) splices.Proposed fix
- // Limit errors to avoid excessive memory usage - if (errors.length > 1000) { - errors.splice(0, 500) - } + // Cap collected errors to avoid excessive memory usage + if (errors.length < 1000) { + errors.push(`Row ${rowIndex + 2}: ${errorMessage}`) + }Move the push inside the guard and remove the outer push on line 166.
packages/core/src/components/search/UploadFileDropdown/UploadFileDropdown.tsx (1)
24-24: Component accepts no props — limits reusability and CMS integration.
UploadFileDropdownis a fully self-contained component with no configurable text, callbacks, or options. The Navbar/SearchInput integration passesfileUploadCardPropsfor label customization, but this component ignores them entirely — all strings are hardcoded. Consider accepting at least anonUseDatacallback and text props to align with the CMS-driven approach used elsewhere in this PR.packages/core/src/components/search/SearchInput/SearchInput.tsx (3)
131-142: Five overlapping boolean states for one upload flow is hard to reason about.
isUploadModalOpen,fileUploadVisible,isUploadOpen, andhasFileall govern visibility/state of the upload UI. Consider consolidating into a single state machine or at minimum a reducer, e.g.:type UploadState = 'idle' | 'dropzone' | 'uploading' | 'completed' const [uploadState, setUploadState] = useState<UploadState>('idle')This would eliminate the stale-state bugs (like the
handleDismississue above) by design.
174-190: Event handlers are recreated every render — wrap withuseCallback.
handleFileSelect,handleDownloadTemplate,handleDismiss, andhandleSearchare all plain closures recreated on each render. Since they're passed as props toFileUploadCard, every parent re-render forces the child to re-render too.Wrap each in
useCallbackwith appropriate dependency arrays. For example:const handleFileSelect = useCallback(async (files: File[]) => { // ... }, [onClearError, onParseFile])As per coding guidelines,
packages/**/src/components/**: "Check rendering performance … use memo/useMemo/useCallback when appropriate."Also applies to: 192-208, 216-237
83-97:errorMessagesusesRecord<string, …>instead ofFileUploadErrorType— loses type safety.The
FileUploadCardPropsinterface in the components package typeserrorMessagesasPartial<Record<FileUploadErrorType, …>>. Usingstringhere means CMS integrators won't get autocompletion or compile-time checks for valid error keys like'unsupported','empty','too-large', etc.Consider importing and reusing
FileUploadErrorType:+import type { FileUploadErrorType } from '@faststore/ui' // ... - errorMessages?: Partial< - Record<string, { title: string; description: string }> - > + errorMessages?: Partial< + Record<FileUploadErrorType, { title: string; description: string }> + >
packages/core/src/components/search/SearchInput/SearchInput.tsx
Outdated
Show resolved
Hide resolved
packages/ui/src/components/molecules/SearchInputField/styles.scss
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/core/src/components/search/SearchInput/SearchInput.tsx (1)
331-345:⚠️ Potential issue | 🟠 MajorSpread order lets CMS props silently override event handlers.
The CMS prop object is spread after the explicit handler props (
onDismiss,onFileSelect,onSearch, etc.). IffileUploadCardPropsever contains keys likeonDismissoronSearch, they will overwrite the functional handlers defined above. Spread the CMS labels first, then the handlers.Also, the
as FileUploadCardPropsassertion still suppresses type checking (previously flagged).Proposed fix — reverse spread order
<FileUploadCard - {...({ - isOpen: isUploadOpen || hasFile || fileUploadVisible, - onDismiss: handleDismiss, - onFileSelect: handleFileSelect, - onDownloadTemplate: handleDownloadTemplate, - formatterFileSize: formatFileSize, - formatterFileName: formatFileName, - onSearch: handleSearch, - isUploading: isCsvProcessing, - hasError: !!csvError, - ...(fileUploadCardProps ?? DEFAULT_FILE_UPLOAD_CARD_PROPS), - } as FileUploadCardProps)} + {...(fileUploadCardProps ?? DEFAULT_FILE_UPLOAD_CARD_PROPS)} + isOpen={isUploadOpen || hasFile || fileUploadVisible} + onDismiss={handleDismiss} + onFileSelect={handleFileSelect} + onDownloadTemplate={handleDownloadTemplate} + formatterFileSize={formatFileSize} + formatterFileName={formatFileName} + onSearch={handleSearch} + isUploading={isCsvProcessing} + hasError={!!csvError} />This also removes the need for the
as FileUploadCardPropsassertion — TypeScript can now validate each prop individually.
🤖 Fix all issues with AI agents
In `@packages/components/src/molecules/Dropzone/Dropzone.tsx`:
- Around line 167-173: The dropzone doesn't expose its disabled state to
assistive tech; update the accessibilityProps logic so that when disabled is
true it sets aria-disabled (e.g., 'aria-disabled': true) and still provides the
role and aria-label fallback (ariaLabel ?? 'Drag and drop files here, or click
to select files'); change the current ternary that returns {} when disabled to
return an object with aria-disabled and the same role/aria-label, and apply the
same change to the other similar accessibilityProps usage (the block around the
second occurrence).
In `@packages/core/src/components/search/SearchInput/SearchInput.tsx`:
- Around line 202-218: The download may fail in Firefox because the anchor
created in handleDownloadTemplate is never attached to the DOM; modify
handleDownloadTemplate (which calls onGenerateTemplate) to append the created
anchor element to document.body before calling a.click(), then remove the anchor
from the DOM afterward (and still call window.URL.revokeObjectURL(url)); ensure
you create the Blob and url as before, set a.href/a.download, appendChild(a),
call a.click(), then removeChild(a) and revoke the url.
In
`@packages/core/src/components/search/UploadFileDropdown/UploadFileDropdown.tsx`:
- Around line 271-273: The Clear button wrapping the UIIcon in
UploadFileDropdown is missing an aria-label; update the component to pass an
accessible label to the Button (e.g., aria-label={labels.clearButtonAriaLabel})
and wire that label into the i18n labels interface and defaults by adding a
clearButtonAriaLabel key to UploadFileDropdownLabels (and any default
labels/translation objects) so consumers can localize it; ensure the Button
usage (function clearData and JSX with Button variant="tertiary" size="small")
reads the new label from props/labels.
- Around line 253-269: In UploadFileDropdown's render where csvData is shown,
remove the unreachable isProcessing conditional that would render <Loader />
(inside the JSX block that uses csvData, formatFileName, formatFileSize, and
labels.getCompletedStatusText) and always render
labels.getCompletedStatusText(formatFileSize(csvData.fileSize),
csvData.totalRows) instead; you can also remove the unused Loader reference in
that JSX branch since onParseFile guarantees isProcessing is false via its
finally block.
In `@packages/storybook/stories/upload-file-dropdown.stories.tsx`:
- Line 1: The import currently includes an unused symbol `useState` (import
React, { useState } from 'react') in the upload-file-dropdown stories; remove
`useState` from the import so it reads simply `import React from 'react'` (or
use the existing default import style) to eliminate the unused import warning
and keep the file clean.
🧹 Nitpick comments (4)
packages/components/src/molecules/Dropzone/Dropzone.tsx (2)
95-98:DropzoneStateis exported but never produced by the component.The component manages
acceptedFilesandfileRejectionsinternally and renders them, but doesn't expose this state to the parent (onlyonFilesAccepted/onFilesRejectedcallbacks fire). Consumers typing againstDropzoneStatewon't be able to obtain a value of this type from the Dropzone itself. If this type is intended for consumers to build their own state management, consider adding a brief JSDoc note clarifying its purpose.
202-224: Built-in file/error lists may not suit most consumers.The component unconditionally renders accepted files and rejection errors as simple
<ul>lists. In most real-world usage (like theFileUploadCardin this PR), consumers will want full control over how files and errors are presented. Consider whether these default lists should be opt-in (e.g., via ashowFileListprop) or removed in favor of render-prop/callback patterns, to avoid consumers needing to hide unwanted DOM with CSS.packages/core/src/components/search/SearchInput/SearchInput.tsx (1)
334-334:isOpenis always truthy whenFileUploadCardrenders.Inside
{fileUploadVisible && ...},fileUploadVisibleis guaranteedtrue, making the|| fileUploadVisiblearm ofisOpen: isUploadOpen || hasFile || fileUploadVisibleredundant —isOpencan never befalsehere. IfFileUploadCardneeds anisOpen={false}state, the condition logic needs rethinking; otherwise simplify.packages/core/src/components/search/UploadFileDropdown/section.module.scss (1)
77-92: Hardcodedmax-width: 500pxfor file name and row details.Consider using a design-token or relative unit instead of the hardcoded
500pxso it adapts to different container widths (e.g., narrower viewports or a resized search dropdown).
packages/core/src/components/search/UploadFileDropdown/UploadFileDropdown.tsx
Show resolved
Hide resolved
packages/core/src/components/search/UploadFileDropdown/UploadFileDropdown.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/core/src/components/search/SearchInput/SearchInput.tsx (1)
331-345:⚠️ Potential issue | 🟠 Major
as FileUploadCardPropstype assertion suppresses type checking.The cast on line 344 hides mismatches between the constructed object and
FileUploadCardProps. If a required prop is added or renamed, TypeScript won't catch it. Remove the assertion and fix any resulting type errors.Also note: the spread order
...(fileUploadCardProps ?? DEFAULT_FILE_UPLOAD_CARD_PROPS)comes afteronSearch,isUploading, etc., so CMS-provided labels can accidentally overwrite functional props likeonDismissoronSearchif they share the same key.♻️ Suggested fix for spread order
<FileUploadCard - {...({ - isOpen: isUploadOpen || hasFile || fileUploadVisible, - onDismiss: handleDismiss, - onFileSelect: handleFileSelect, - onDownloadTemplate: handleDownloadTemplate, - formatterFileSize: formatFileSize, - formatterFileName: formatFileName, - onSearch: handleSearch, - isUploading: isCsvProcessing, - hasError: !!csvError, - ...(fileUploadCardProps ?? DEFAULT_FILE_UPLOAD_CARD_PROPS), - } as FileUploadCardProps)} + {...(fileUploadCardProps ?? DEFAULT_FILE_UPLOAD_CARD_PROPS)} + isOpen={isUploadOpen || hasFile || fileUploadVisible} + onDismiss={handleDismiss} + onFileSelect={handleFileSelect} + onDownloadTemplate={handleDownloadTemplate} + formatterFileSize={formatFileSize} + formatterFileName={formatFileName} + onSearch={handleSearch} + isUploading={isCsvProcessing} + hasError={!!csvError} />This ensures CMS labels are spread first and functional props can't be overridden. It also removes the need for the
ascast.
🤖 Fix all issues with AI agents
In `@packages/components/src/molecules/Dropzone/Dropzone.tsx`:
- Around line 168-177: The accessibilityProps currently assigns role="button"
even when ariaLabel is missing, causing an unlabeled interactive element; update
the logic that builds accessibilityProps (referencing accessibilityProps,
ariaLabel and disabled in Dropzone.tsx) to provide a sensible fallback
accessible name when ariaLabel is falsy (for example derive a default like "Drop
files" or use an existing prop/children text) and set 'aria-label' to that
fallback so every role="button" instance always has an accessible name; ensure
both the disabled and non-disabled branches use the same fallback behavior.
In `@packages/core/src/components/search/SearchInput/SearchInput.tsx`:
- Around line 230-249: handleSearch dispatches a global CustomEvent
('faststore:file-search') with a payload and leaves a TODO; remove the ambiguous
public event or document its contract and complete integration: either (A)
delete the window.dispatchEvent call and the trailing TODO and rely solely on
the typed onFileSearch callback (onFileSearch and csvData.payload shape:
fileName, totalRows, fileSize, data), or (B) keep the CustomEvent but add a
documented exported type/JS Doc for the event detail shape and ensure listeners
are registered/cleaned up elsewhere; update handleSearch to reflect the chosen
approach and remove the TODO comment.
- Around line 202-220: handleDownloadTemplate currently only logs errors to
console and provides no user feedback; update it to surface failures the same
way UploadFileDropdown.tsx does by calling the shared toast/error UI when
onGenerateTemplate or the download process throws. In the catch block of
handleDownloadTemplate, replace or supplement console.error with the project
toast method (or set an error state) used elsewhere (e.g., the toast invocation
pattern from UploadFileDropdown.tsx) and include a clear message like "Failed to
download template" plus the error details so users see a visible failure
notification.
In `@packages/storybook/stories/upload-file-dropdown.stories.tsx`:
- Around line 2-9: Remove the unused Loader import from the named import list in
the stories file: locate the import statement that destructures Button,
Dropzone, Icon, Loader, SearchDropdown, UIProvider (the symbol "Loader" in that
list) and delete "Loader" from that destructuring so the module no longer
imports an unused symbol, keeping the rest of the imports unchanged.
🧹 Nitpick comments (5)
packages/storybook/stories/upload-file-dropdown.stories.tsx (3)
110-110: Consider using Storybookaction()instead ofconsole.log.Using
action('filesAccepted')from@storybook/addon-actionssurfaces events in the Storybook Actions panel and avoids stray console noise.Also applies to: 169-169
30-51: Duplicated interface may drift from source.The comment on lines 22-29 explains the rationale — appreciated. Just flag that
UploadFileDropdownLabelsin@faststore/coreis the source of truth (seeUploadFileDropdown.tsx:26-70). If new label props are added there, this story won't surface them in the reference table until manually synced.A
// TODO: keep in sync with UploadFileDropdownLabels from@faststore/core`` or a lint/test that compares the two would reduce drift risk.
80-86:formatFileSizeduplicated from core utilities.This is identical to
packages/core/src/utils/utilities.ts:160-168. Fine for a self-contained story, but worth a brief inline comment pointing to the canonical source so future maintainers know where to look if the logic changes.packages/core/src/components/search/UploadFileDropdown/UploadFileDropdown.tsx (1)
161-174: Memoize handlers passed as props toUIDropzone.
handleFilesAcceptedandhandleFilesRejectedare recreated every render and passed toUIDropzoneviaonFilesAccepted/onFilesRejected. This can cause unnecessary re-renders of the dropzone subtree. Wrap them withuseCallback.♻️ Suggested refactor
- const handleFilesAccepted = async (files: File[]) => { + const handleFilesAccepted = useCallback(async (files: File[]) => { if (files.length === 0) return setCsvData(null) onClearError() const file = files[0] const parsedData = await onParseFile(file) if (parsedData) { setCsvData(parsedData) } - } + }, [onParseFile, onClearError]) - const handleFilesRejected = ( + const handleFilesRejected = useCallback(( fileRejections: DropzoneState['fileRejections'] ) => { // ... body unchanged ... - } + }, [pushToast, labels.toastRejectionTitle, labels.toastRejectionDefaultMessage, labels.toastFileTooLargeMessage, labels.toastFileInvalidTypeMessage, labels.toastTooManyFilesMessage])As per coding guidelines,
packages/**/src/components/**: "Check rendering performance … use memo/useMemo/useCallback when appropriate."Also applies to: 176-204
packages/components/src/molecules/Dropzone/Dropzone.tsx (1)
206-228: Using arrayindexaskeyfor file lists — acceptable here but worth noting.Since
acceptedFilesfromreact-dropzonecan accumulate across multiple drops without resetting, the rendered list may grow unexpectedly. If the Dropzone is intended as a single-use upload trigger (e.g., for the CSV flow), consider whether the parent should control displayed files instead.For the keys themselves,
file.nameisn't guaranteed unique either, soindexis fine for now.
packages/core/src/components/search/UploadFileDropdown/UploadFileDropdown.tsx
Show resolved
Hide resolved
packages/core/src/components/search/UploadFileDropdown/UploadFileDropdown.tsx
Outdated
Show resolved
Hide resolved
packages/core/src/components/search/UploadFileDropdown/UploadFileDropdown.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@packages/core/src/components/search/SearchInput/SearchInput.tsx`:
- Around line 336-349: The FileUploadCard prop spread currently places
...(fileUploadCardProps ?? DEFAULT_FILE_UPLOAD_CARD_PROPS) after explicit props
so CMS/default props can override handlers; change the JSX so the spread appears
before the explicit props (e.g., move the ...(fileUploadCardProps ??
DEFAULT_FILE_UPLOAD_CARD_PROPS) above onDismiss, onFileSelect,
onDownloadTemplate, onSearch, isOpen, isUploading, hasError) ensuring the
explicit handlers in this component (handleDismiss, handleFileSelect,
handleDownloadTemplate, handleSearch, formatFileSize, formatFileName,
isUploadOpen/hasFile/fileUploadVisible logic, isCsvProcessing, csvError) always
take precedence.
🧹 Nitpick comments (2)
packages/components/src/hooks/useCSVParser.ts (1)
137-175: Zero quantity is accepted — intentional for bulk orders?Line 153 rejects negative quantities but allows
quantity = 0. For a bulk-order/quick-order flow, a zero-quantity row is likely a no-op. Consider rejectingnumericQuantity <= 0unless there's a use case for adding zero-quantity items.- if (Number.isNaN(numericQuantity) || numericQuantity < 0) { + if (Number.isNaN(numericQuantity) || numericQuantity <= 0) {packages/components/src/index.ts (1)
2-9: Redundant re-exports:export *already covers these.Line 2's
export * from './hooks're-exports everything fromhooks/index.ts, which already includesuseCSVParser,DEFAULT_FILE_UPLOAD_OPTIONS,useFileUpload, andFileUploadOptions. The explicit re-exports on lines 3–9 are duplicates. They won't break anything, but the type-only re-exports (CSVData,CSVParserError,CSVParserOptions) are the only ones actually needed here since they aren't exported fromhooks/index.ts.Lines 8–9 (
DEFAULT_FILE_UPLOAD_OPTIONSandFileUploadOptions) can be removed sincehooks/index.tsalready exports them.Suggested cleanup
export * from './hooks' export type { CSVData, CSVParserError, CSVParserOptions, } from './hooks/useCSVParser' -export { DEFAULT_FILE_UPLOAD_OPTIONS } from './hooks/useFileUpload' -export type { FileUploadOptions } from './hooks/useFileUpload'
## Summary - Replace hardcoded version references (`^3.97.0-dev.2`) with `workspace:*` protocol for `@faststore/components` and `@faststore/ui` in `packages/ui/package.json` and `packages/storybook/package.json` - This ensures local packages are always resolved from the monorepo workspace during development ## Motivation Extracted from PR #3117 as requested in [code review](#3117) by @lariciamota, so we can identify possible problems easier by isolating this change. ## Changed files - `packages/ui/package.json` — `@faststore/components` dependency and peerDependency changed to `workspace:*` - `packages/storybook/package.json` — `@faststore/components` and `@faststore/ui` dependencies changed to `workspace:*` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Switched internal package references in Storybook and UI to workspace-based declarations to improve build consistency. * Aligned dependency declarations across the monorepo to reduce version mismatch risks and streamline local development and local testing. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Larícia Mota <laricia.mota@vtex.com.br>
lariciamota
left a comment
There was a problem hiding this comment.
Deixando aprovado já, mas dá um rebase antes da gente mergear pfv, já que o #3209 foi mergeado na dev :D
What's the purpose of this pull request?
This pull request introduces a new file upload and CSV parsing feature to the
componentspackage, including reusable hooks and UI components. The main changes are the addition of aFileUploadCardcomponent for uploading files, auseCSVParserhook for parsing CSV files in a web worker, and auseFileUploadhook for managing file upload errors and validation. These new utilities are exported for use in other parts of the application.This PR it was created from #3109
How it works?
New hooks and utilities:
useCSVParserhook for parsing CSV files in a web worker, with error handling and CSV template generation.useFileUploadhook to manage file upload errors, validation, and rejection reasons.New UI components:
FileUploadCardcomponent for file selection, drag-and-drop, and template download, with error state and file validation.FileUploadCardandFileUploadStatuscomponents and their types.Dependency updates:
papaparseto dependencies for running CSV parsing in a web worker.How to test it?
Starters Deploy Preview
References
Checklist
You may erase this after checking them all 😉
PR Title and Commit Messages
feat,fix,chore,docs,style,refactor,ciandtestPR Description
breaking change,bug,contributing,performance,documentation..Dependencies
pnpm-lock.yamlfile when there were changes to the packagesDocumentation
@Mariana-Caetanoto review and update (Or submit a doc request)Summary by CodeRabbit
New Features
Enhancements
Refactor
Style