feat: file upload by agent (first gen)#416
Conversation
|
Visit the preview URL for this PR (updated for commit ab92fe8): https://react-koobiq-next--prs-416-8r0hrlqz.web.app (expires Wed, 15 Jul 2026 09:20:22 GMT) 🔥 via Firebase Hosting GitHub Action 🌎 Sign: fc29847d4a9e5cb1adf458c76a9b681c76e2eeff |
There was a problem hiding this comment.
Pull request overview
This PR introduces a new FileUpload component suite to @koobiq/react-components, providing single- and multiple-file upload UIs plus supporting primitives (dropzones, input wrapper, hooks), localization, validators, and API extraction/guard wiring.
Changes:
- Adds
SingleFileUploadandMultipleFileUploadcomponents with drag-and-drop support, optional full-screen/local dropzones, progress display, and external-validation patterns. - Introduces supporting primitives and hooks (
FileInput,useFileDrop,useFileList,useFileUploadState, locale resolution, etc.) plus locale bundles and docs/stories. - Registers FileUpload in API Extractor and adds the generated public API report.
Reviewed changes
Copilot reviewed 58 out of 58 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| tools/public_api_guard/components/FileUpload.api.md | New generated API report for FileUpload public surface |
| tools/api-extractor/config.json | Registers FileUpload for API extraction |
| packages/components/src/components/index.ts | Re-exports FileUpload from the components barrel |
| packages/components/src/components/FileUpload/validators.ts | Adds file validators (maxFileSize, isCorrectExtension) |
| packages/components/src/components/FileUpload/utils.ts | Shared utilities (data size formatting, DnD extraction, directory unwrap) |
| packages/components/src/components/FileUpload/types.ts | Public types for FileUpload components/primitives |
| packages/components/src/components/FileUpload/SingleFileUpload.tsx | Implements single-file uploader component |
| packages/components/src/components/FileUpload/SingleFileUpload.test.tsx | Tests for SingleFileUpload behavior and a11y basics |
| packages/components/src/components/FileUpload/SingleFileUpload.module.css | Single uploader style overrides via CSS custom properties |
| packages/components/src/components/FileUpload/primitives.test.tsx | Tests for primitives/hooks/utilities/validators |
| packages/components/src/components/FileUpload/MultipleFileUpload.tsx | Implements multiple-file uploader component |
| packages/components/src/components/FileUpload/MultipleFileUpload.test.tsx | Tests for MultipleFileUpload list behavior and interactions |
| packages/components/src/components/FileUpload/MultipleFileUpload.module.css | Multiple uploader layout/list styling |
| packages/components/src/components/FileUpload/locales.ts | Bundled locale configs + default/fallback mapping |
| packages/components/src/components/FileUpload/locales.test.tsx | Tests for locale resolution and overrides |
| packages/components/src/components/FileUpload/intl.json | Localized strings for dropzone overlay title / remove label |
| packages/components/src/components/FileUpload/index.ts | Public exports for FileUpload package surface |
| packages/components/src/components/FileUpload/hooks/useFullScreenDropzone/useFullScreenDropzone.ts | Hook for document-level full-screen dropzone behavior |
| packages/components/src/components/FileUpload/hooks/useFullScreenDropzone/index.ts | Barrel export for useFullScreenDropzone |
| packages/components/src/components/FileUpload/hooks/useFileUploadState/useFileUploadState.ts | Hook for touched/dirty/error visibility state |
| packages/components/src/components/FileUpload/hooks/useFileUploadState/index.ts | Barrel export for useFileUploadState |
| packages/components/src/components/FileUpload/hooks/useFileUploadLocale/useFileUploadLocale.ts | Hooks to resolve locale config + caption context |
| packages/components/src/components/FileUpload/hooks/useFileUploadLocale/index.ts | Barrel export for locale hooks |
| packages/components/src/components/FileUpload/hooks/useFileUploadItem/useFileUploadItem.ts | Hook deriving loading/progress display state |
| packages/components/src/components/FileUpload/hooks/useFileUploadItem/index.ts | Barrel export for useFileUploadItem |
| packages/components/src/components/FileUpload/hooks/useFileList/useFileList.ts | Headless list-state hook for upload items |
| packages/components/src/components/FileUpload/hooks/useFileList/index.ts | Barrel export for useFileList |
| packages/components/src/components/FileUpload/hooks/useFileInput/useFileInput.ts | Hook mapping props/context to a native file input + reset |
| packages/components/src/components/FileUpload/hooks/useFileInput/index.ts | Barrel export for useFileInput |
| packages/components/src/components/FileUpload/hooks/useFileDrop/useFileDrop.ts | Hook attaching native DnD listeners and dragover state |
| packages/components/src/components/FileUpload/hooks/useFileDrop/index.ts | Barrel export for useFileDrop |
| packages/components/src/components/FileUpload/hooks/index.ts | Aggregated hook exports |
| packages/components/src/components/FileUpload/FileUploadContext.ts | Context for upload primitives configuration |
| packages/components/src/components/FileUpload/FileUpload.stories.tsx | Storybook stories demonstrating features and patterns |
| packages/components/src/components/FileUpload/FileUpload.module.css | Shared base styling for the upload UI |
| packages/components/src/components/FileUpload/FileUpload.mdx | Documentation page for FileUpload |
| packages/components/src/components/FileUpload/errorPredicates.ts | Error-visibility predicate helpers |
| packages/components/src/components/FileUpload/dropzones.test.tsx | Tests for full-screen and local dropzone behavior |
| packages/components/src/components/FileUpload/constants.ts | Shared constants (name truncation tail length, focus return delay) |
| packages/components/src/components/FileUpload/components/LocalDropzone/LocalDropzone.tsx | Scoped/host-positioned dropzone overlay component |
| packages/components/src/components/FileUpload/components/LocalDropzone/LocalDropzone.module.css | Local dropzone overlay/host styling |
| packages/components/src/components/FileUpload/components/LocalDropzone/index.ts | Barrel export for LocalDropzone |
| packages/components/src/components/FileUpload/components/index.ts | Aggregated primitive component exports |
| packages/components/src/components/FileUpload/components/FullScreenDropzone/index.ts | Barrel export for FullScreenDropzone |
| packages/components/src/components/FileUpload/components/FullScreenDropzone/FullScreenDropzone.tsx | Portalled full-screen overlay component |
| packages/components/src/components/FileUpload/components/FullScreenDropzone/FullScreenDropzone.module.css | Full-screen overlay styling |
| packages/components/src/components/FileUpload/components/FileUploadProvider/index.ts | Barrel export for FileUploadProvider |
| packages/components/src/components/FileUpload/components/FileUploadProvider/FileUploadProvider.tsx | Provider for FileUploadContext values |
| packages/components/src/components/FileUpload/components/FileUploadItem/index.ts | Barrel export for FileUploadItem |
| packages/components/src/components/FileUpload/components/FileUploadItem/FileUploadItem.tsx | Renders a file row (icon/progress, name, size, remove) |
| packages/components/src/components/FileUpload/components/FileUploadItem/FileUploadItem.module.css | File item row styling |
| packages/components/src/components/FileUpload/components/FileUploadCaption/index.ts | Barrel export for FileUploadCaption |
| packages/components/src/components/FileUpload/components/FileUploadCaption/FileUploadCaption.tsx | Caption rendering (text + browse links) |
| packages/components/src/components/FileUpload/components/FileInput/index.ts | Barrel export for FileInput |
| packages/components/src/components/FileUpload/components/FileInput/FileInput.tsx | Label-wrapped hidden file input primitive |
| packages/components/src/components/FileUpload/components/DropzoneContent/index.ts | Barrel export for DropzoneContent |
| packages/components/src/components/FileUpload/components/DropzoneContent/DropzoneContent.tsx | Overlay content (icon/title/caption) with focus trapping |
| packages/components/src/components/FileUpload/components/DropzoneContent/DropzoneContent.module.css | DropzoneContent layout styling |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export const extractDroppedFiles = async ( | ||
| dataTransfer: DataTransfer | ||
| ): Promise<FileWithPath[]> => { | ||
| const entries = Array.from(dataTransfer.items) | ||
| .filter((item) => item.kind === 'file') | ||
| .map((item) => item.webkitGetAsEntry()) | ||
| .filter((entry): entry is FileSystemEntry => entry != null); | ||
|
|
||
| const groups = await Promise.all(entries.map(unwrapDirectory)); | ||
|
|
||
| return groups.reduce<FileWithPath[]>((acc, group) => acc.concat(group), []); | ||
| }; |
| const reader = next.createReader(); | ||
|
|
||
| queue.push( | ||
| new Promise<FileSystemEntry[]>((resolve, reject) => | ||
| reader.readEntries(resolve, reject) | ||
| ) | ||
| ); |
| if (event.dataTransfer && event.dataTransfer.items.length > 0) { | ||
| void extractDroppedFiles(event.dataTransfer).then((files) => | ||
| onDropRef.current(files) | ||
| ); | ||
| } |
| if (event.dataTransfer && event.dataTransfer.items.length > 0) { | ||
| void extractDroppedFiles(event.dataTransfer).then((files) => | ||
| onDropRef.current(files) | ||
| ); | ||
| } |
| if (event.dataTransfer && event.dataTransfer.items.length > 0) { | ||
| void extractDroppedFiles(event.dataTransfer).then((files) => | ||
| connectedTo.current?.addFiles(files) | ||
| ); | ||
| } |
| disabled?: boolean; | ||
| }; | ||
|
|
||
| type SingleFileUploadOwnProps = FileUploadSharedProps & { |
| Omit<ComponentPropsWithoutRef<'div'>, 'onChange' | 'defaultValue' | 'color'> | ||
| >; | ||
|
|
||
| type MultipleFileUploadOwnProps = FileUploadSharedProps & { |
| <FileInput | ||
| id={inputId} | ||
| accept={accept} | ||
| multiple | ||
| isDisabled={isDisabled} |
| // Warning: (ae-forgotten-export) The symbol "MultipleFileUploadOwnProps" needs to be exported by the entry point index.d.ts | ||
| // | ||
| // @public (undocumented) | ||
| export type MultipleFileUploadProps = ExtendableProps<MultipleFileUploadOwnProps, Omit<ComponentPropsWithoutRef<'div'>, 'onChange' | 'defaultValue' | 'color'>>; | ||
|
|
|
|
||
| if (process.env.NODE_ENV !== 'production' && 'disabled' in props) { | ||
| deprecate( | ||
| 'SingleFileUpload: the "disabled" prop is deprecated. Use "isDisabled" prop to replace it.' |
There was a problem hiding this comment.
кажется странным, что у нового компонента disabled уже deprecated.
| markTouched(); | ||
|
|
||
| // Return focus to the input once the empty state (with its input) re-renders. | ||
| setTimeout(() => browseInputRef.current?.focus(), FOCUS_RETURN_DELAY); |
There was a problem hiding this comment.
setTimeout с фиксированной задержкой выглядит хрупко
| * Validation is external — `accept` is a browse hint only and is not enforced on | ||
| * drop. | ||
| */ | ||
| export const MultipleFileUpload = forwardRef< |
There was a problem hiding this comment.
SingleFileUpload и MultipleFileUpload лучше объединить в единый FileUpload с allowsMultiple: это режимы одного контрола, а их разделение раздувает API и дублирует реализацию.
| import type { ExtendableProps } from '@koobiq/react-core'; | ||
|
|
||
| /** A single file tracked by an upload component. */ | ||
| export interface FileItem { |
There was a problem hiding this comment.
FileItem слишком жёстко зашивает модель данных в компонент: пользователь обязан хранить file, loading, progress и hasError в заданном формате. Для Koobiq React это нетипично и ограничивает кастомные модели и состояния.
| hasError?: boolean; | ||
| /** Whether this file is currently loading; replaces its icon with a progress indicator. */ | ||
| loading?: boolean; |
There was a problem hiding this comment.
Boolean-поля названы неконсистентно с API библиотеки: вместо loading ожидается isLoading, а для состояния ошибки — isInvalid вместо hasError.
| {caption ? <div className={s.hint}>{caption}</div> : null} | ||
| {invalid && errorMessage ? ( | ||
| <div className={s.error}>{errorMessage}</div> | ||
| ) : null} |
There was a problem hiding this comment.
Caption и error message рендерятся вручную, хотя в библиотеке для этого есть FormField.Caption и FormField.Error. Стоит переиспользовать FormField, чтобы сохранить единые стили, семантику и ARIA-связи с контролом.
| const error = submitted ? validate(items.map((item) => item.file)) : null; | ||
|
|
||
| return ( | ||
| <form |
There was a problem hiding this comment.
Можно показать композицию с уже готовым компонентом Form.
| <div style={{ inlineSize: 360 }}> | ||
| <MultipleFileUpload dropTargetRef={target} /> | ||
| </div> | ||
| <LocalDropzone |
There was a problem hiding this comment.
Локальная и fullscreen dropzone — скорее разные режимы одного FileUpload. Не хочется выносить их в отдельные публичные компоненты — overlay и portal можно оставить деталями внутренней реализации.
| onBlur={field.onBlur} | ||
| isInvalid={fieldState.invalid} | ||
| errorMessage={fieldState.error?.message} | ||
| shouldShowError={() => fieldState.isTouched || formState.isSubmitted} |
There was a problem hiding this comment.
shouldShowError лишний — лучше использовать стандартные validate/validationBehavior и FormField, как в остальных контролах.
| * Presentational helper deriving the display state for a single file item | ||
| * (loading spinner vs. icon, and the progress value). | ||
| */ | ||
| export function useFileUploadItem(item: FileItem): UseFileUploadItemReturn { |
There was a problem hiding this comment.
useFileUploadItem выглядит лишней абстракцией: он лишь читает loading и нормализует progress, не управляя состоянием или поведением. проще оставить внутри FileUploadItem.
No description provided.