Docyrus

FileAttachmentPanel

Record attachments panel with pick-and-upload from files, photos or the camera, per-file progress with retry, list and grid views, and delete confirmation.

iOSAndroid
Preview FileAttachmentPanel on your device

Scan with Expo Go

Download Expo Go, then scan the QR code to preview native components.

Installation

pnpm dlx @docyrus/cli add @docyrus/rn-file-attachment-panel
Required Packages(2 packages)
pnpm add expo-document-picker expo-image-picker

Both pickers are optional peers loaded lazily. The Add sheet offers only the sources that are installed:

SourcePackageSheet item
documentexpo-document-pickerBrowse files (multi-select, filtered by the MIME part of accept)
libraryexpo-image-pickerPhoto library (images + videos, multi-select)
cameraexpo-image-pickerTake photo

With a single installed source the Add button opens that picker directly; with none (and no onPickFiles) the Add button is hidden.

Usage

import {
  FileAttachmentPanel,
  type DocyrusFile,
  type NativeFile
} from '@/components/docyrus-native/file-attachment-panel';

async function uploadFile(file: NativeFile, options?: { onProgress: (progress: number) => void }): Promise<DocyrusFile> {
  const form = new FormData();

  form.append('file', { uri: file.uri, name: file.name, type: file.type } as never);

  // Use XMLHttpRequest (or expo-file-system uploadAsync) to report real progress.
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();

    xhr.open('POST', `${apiUrl}/v1/apps/base/data-sources/contact/items/${recordId}/files`);
    xhr.setRequestHeader('Authorization', `Bearer ${token}`);
    xhr.upload.onprogress = event => options?.onProgress((event.loaded / event.total) * 100);
    xhr.onload = () => (xhr.status < 300 ? resolve(JSON.parse(xhr.responseText).data) : reject(new Error('Upload failed')));
    xhr.onerror = () => reject(new Error('Network error'));
    xhr.send(form);
  });
}

<FileAttachmentPanel
  files={files}
  maxFiles={10}
  maxFileSize={20 * 1024 * 1024}
  accept={['image/*', 'application/pdf']}
  onUploadFile={uploadFile}
  onDeleteFile={id => deleteFile(id)}
  isDeletePending={deleteMutation.isPending}
  onValidationError={message => toast.error(message)} />

Behaviour:

  • Add → source sheet → picker → every file is validated (maxFileSize, accept, remaining maxFiles slots; rejections go to onValidationError) → onUploadFile is called per file in parallel.
  • Each upload gets a progress row. Without onProgress reports the bar sits at an indeterminate 50% (like web); a finished upload shows a check for 1.5 s. A rejected promise turns the row into an error row with Retry (re-runs onUploadFile with the same file) and Dismiss.
  • Tapping a file calls onFileOpen, or Linking.openURL(signed_url) when onFileOpen is omitted.
  • Delete asks through DeleteConfirmDialog (objectName="file"); the dialog closes when onDeleteFile resolves and shows a spinner while isDeletePending.
  • Uncontrolled view mode starts in grid when any file is an image, else list.

API Reference

FileAttachmentPanelProps

PropTypeDefaultDescription
filesDocyrusFile[]-Attached files (required)
titlestring'Attachments'Header title
editablebooleantrueShow the Add button and delete affordances
isLoadingbooleanfalseShow the loading spinner + skeleton rows
maxHeightnumber-Caps the panel height (dp) and scrolls its content. Omit to render inline (web default is 560)
maxFileSizenumber52428800 (50 MB)Maximum file size in bytes
acceptstring | string[]-Accepted types — a MIME list (['image/*', 'application/pdf']) or a web accept string ('image/*,.pdf'). Extension patterns match the file name
maxFilesnumber-Maximum number of files. Shown as n/max in the counter badge; Add is disabled when reached and extra picks are reported via onValidationError
onUploadFile(file: NativeFile, options?: UploadFileOptions) => Promise<DocyrusFile>-Uploads one picked file. Report progress through options.onProgress(0–100). Required for the Add button
onDeleteFile(fileId: string) => void | Promise<void>-Called after the delete confirmation. A rejection keeps the dialog open
onInsertExternalFiles(files: ExternalFilePayload[]) => void | Promise<void>-API parity with web. Native has no OneDrive / Google Drive picker, so it is never called
onFileOpen(file: DocyrusFile) => voidLinking.openURL(signed_url)File tap handler
isDeletePendingbooleanfalsePending state of the delete confirmation (and the row being deleted)
classNamestring-Container classes
oneDriveConfigOneDrivePickerConfig-API parity with web — inert on native
googleDriveConfigGoogleDrivePickerConfig-API parity with web — inert on native
viewMode'list' | 'grid'autoNative extra. Controlled view mode; uncontrolled = grid when images are present
onViewModeChange(mode: ViewMode) => void-Native extra. View toggle callback
uploadingFilesUploadingFile[]-Native extra. Controlled upload rows — when set, they replace the internally tracked uploads (rendered without Retry / Dismiss)
onPickFiles() => Promise<NativeFile[]>-Native extra. Custom picker that replaces the source sheet; return [] on cancel
onValidationError(error: string) => void-Native extra. A picked file was rejected (size, type, maxFiles)
styleViewStyle-Container style

DocyrusFile

The web record shape; everything except id / file_name is optional on native.

FieldTypeDescription
idstringUnique identifier
file_namestringFile name
file_typestringMIME type — drives the coloured file-type icon and image thumbnails
file_sizenumberSize in bytes
signed_urlstring | nullSigned URL — thumbnails and the default open action
source'local' | 'microsoft_onedrive' | 'google_drive'Origin; cloud sources show a brand badge
file_dataRecord<string, unknown> | nullProvider metadata
created_onstringISO creation date
record_idstring | nullOwning record

NativeFile

FieldTypeDescription
uristringLocal file URI (file://, content://, ph://)
namestringFile name with extension
typestringMIME type (application/octet-stream when unknown)
sizenumberSize in bytes, when known

UploadFileOptions

FieldTypeDescription
onProgress(progress: number) => voidReport upload progress (0–100)

UploadingFile

FieldTypeDescription
idstringRow id
fileNativeFileFile being uploaded
progressnumber0–100
status'uploading' | 'complete' | 'error'Row state
errorstringError message (error rows)

ExternalFilePayload

FieldTypeDescription
source'microsoft_onedrive' | 'google_drive'Provider
file_namestringFile name
file_typestringMIME type
signed_urlstringProvider URL
file_dataRecord<string, unknown>Provider metadata
expire_innumberURL lifetime

Components

ComponentDescription
FileAttachmentPanelHeader (title, counter, list/grid toggle, Add), file list or 3-column grid, upload rows, delete confirmation

Type Exports

TypeDescription
FileAttachmentPanelPropsPanel props
DocyrusFileAttached file record
NativeFilePicked file ({ uri, name, type, size? })
UploadFileOptionsSecond argument of onUploadFile
UploadingFileUpload row
ViewMode'list' | 'grid'
ExternalFilePayloadCloud file payload (API parity)
OneDrivePickerConfig / GoogleDrivePickerConfigWeb picker configs (API parity, inert)
FileAttachmentPanelViewModeDeprecated alias of ViewMode
FileAttachmentPickedFileDeprecated alias of NativeFile

i18n keys

Via useUiTranslation(): ui.fileAttachment.title, add, addFiles, sourceFiles, sourcePhotos, sourceCamera, listView, gridView, loadingFiles, noFiles, emptyHint, emptyReadOnly, uploadFailed, retry, dismiss, fileTooLarge ({name}, {size}), fileTypeNotAccepted ({name}), maxFilesReached ({count}), plus ui.common.delete.

Breaking changes

  • The component moved to a folder; import path @/components/docyrus-native/file-attachment-panel is unchanged.
  • title default 'Files' → 'Attachments' (web).
  • maxFileSize now defaults to 50 MB (was unlimited).
  • uploadingFiles entries changed from { id, name, progress: 0–1 } to the web UploadingFile shape { id, file: NativeFile, progress: 0–100, status, error? }.
  • Upload failures now render as an error row with Retry / Dismiss and are no longer reported through onValidationError (which now only covers rejected picks).
  • onDeleteFile is confirmed through DeleteConfirmDialog instead of Alert.alert; the dialog waits for the returned promise.
  • Tapping a file without onFileOpen now opens signed_url (was a no-op).
  • Uncontrolled view mode defaults to grid when images are present (was always list).
  • The Add button now opens a source sheet (files / photos / camera, depending on installed pickers) instead of the document picker directly; it is disabled (not hidden) when maxFiles is reached.
  • FileAttachmentPanelViewMode and FileAttachmentPickedFile are deprecated aliases of ViewMode / NativeFile.

On this page