npm package: @rezaser/nexus-filepicker
Description: Vue 3 library for a Nextcloud-style attachment picker, file and folder uploads via WebDAV, and resumable downloads (e.g. for chat or meetings).
Latest version: 0.1.0

# @nexus/nexus-filepicker

Vue 3 library for a Nextcloud-style attachment picker, file and folder uploads via WebDAV, and resumable downloads (e.g. for chat or meetings).

## Installation

```bash
npm install @nexus/nexus-filepicker
```

**Peer dependency:** Vue 3 (`^3.5.0`).

When using the attachment picker UI, import the package styles once (e.g. in your app entry or root component):

```js
import '@nexus/nexus-filepicker/dist/nexus-filepicker.css'
```

## Quick start

Register the attachment picker and render it with your DAV config:

```js
import { registerAttachmentPicker, renderAttachment } from '@nexus/nexus-filepicker'
import '@nexus/nexus-filepicker/dist/nexus-filepicker.css'

registerAttachmentPicker()
renderAttachment({
  baseUrl: 'https://your-nextcloud.example.com',
  remoteURL: 'https://dav.example.com/remote.php/dav',
  rootPath: '/files/User',
  token: 'your-auth-token',
}, document.getElementById('app'))
```

Or use the upload API only (no UI):

```js
import { uploadOne } from '@nexus/nexus-filepicker'

const config = {
  remoteURL: 'https://dav.example.com/remote.php/dav',
  rootPath: '/files/User',
  token: 'your-auth-token',
  context: 'context application',
  contextId: 'id of chat-calendar',
  contextName: 'name of group',
  owner: 'your-user-id(sub)',
}
await uploadOne(config, 'folder/file.txt', file)
```

## Example: React

The picker is a Vue-based custom element; you can use it from React by rendering into a DOM node and using the imperative upload/download APIs. Config can include `context`, `contextName`, `contextId`, `owner`, and `multiSelect` for context-aware uploads.

```jsx
import { useState, useRef, useEffect } from 'react'
import {
  renderAttachment,
  getUploaderForConfig,
  uploadOne,
  UploadStatus,
  createDownloadController,
} from '@nexus/nexus-filepicker'
import '@nexus/nexus-filepicker/dist/nexus-filepicker.css'

const config = {
  remoteURL: 'https://localhost/remote.php/dav',
  rootPath: '/files/your-user-id',
  token: 'your-jwt-or-token',
  context: 'calendar',
  contextId: '123',
  contextName: 'test',
  multiSelect: true,
  owner: 'your-user-id',
}

function App() {
  const containerRef = useRef(null)
  const attachmentElRef = useRef(null)
  const uploaderRef = useRef(null)

  if (!uploaderRef.current) {
    uploaderRef.current = getUploaderForConfig(config)
  }

  useEffect(() => {
    const container = containerRef.current
    if (!container) return
    if (attachmentElRef.current?.parentNode === container) {
      container.removeChild(attachmentElRef.current)
    }
    const el = renderAttachment({ ...config, baseUrl: 'https://your-nextcloud.example.com' }, container)
    attachmentElRef.current = el
    return () => {
      if (attachmentElRef.current?.parentNode === container) {
        container.removeChild(attachmentElRef.current)
      }
    }
  }, [])

  // Poll uploader queue for progress
  const [uploads, setUploads] = useState([])
  useEffect(() => {
    const uploader = uploaderRef.current
    if (!uploader) return
    const intervalId = setInterval(() => {
      const queue = uploader.queue || []
      setUploads(queue.map((u) => ({
        id: u.source || u.file?.name,
        name: u.file?.name,
        percent: u.size ? Math.round((u.uploaded / u.size) * 100) : 0,
        status: u.status,
        uploadRef: u,
      })))
    }, 500)
    return () => clearInterval(intervalId)
  }, [])

  // Notify when an upload finishes
  useEffect(() => {
    const notifier = (upload) => {
      if (upload.status === UploadStatus.FINISHED) {
        // e.g. setUploadedItems((prev) => [...prev, upload])
      }
    }
    uploaderRef.current?.addNotifier(notifier)
  }, [])

  const handleUpload = () => {
    if (!file) return
    uploadOne(config, file.name, file)
  }

  // Download with progress, pause, resume, cancel
  const onDownload = async (link, token) => {
    const ctrl = createDownloadController(link, token, {
      onProgress: ({ percent, loaded, total, paused }) => {
        // Update your UI: percent, loaded, total, paused
      },
    })
    try {
      await ctrl.start()
    } catch (err) {
      if (err?.name !== 'AbortError') console.error(err)
    }
  }
  // Use ctrl.pause(), ctrl.resume(), ctrl.cancel() as needed

  return (
    <>
      <div ref={containerRef} />
      {/* Upload list (uploads + status), uploaded items table, download buttons */}
    </>
  )
}
```

* **Attachment picker:** Mount once in `useEffect` with a ref; clean up on unmount. Pass the same config plus `baseUrl` into `renderAttachment`.
* **Upload progress:** Read `uploader.queue` (e.g. on an interval); each item has `status` (`UploadStatus`), `uploaded`, `size`, `file`, and `.cancel()`.
* **Finished uploads:** `uploader.addNotifier(notifier)`; in the callback, check `upload.status === UploadStatus.FINISHED`.
* **Download:** `createDownloadController(link, token, { onProgress })` then `ctrl.start()`. Use `ctrl.pause()`, `ctrl.resume()`, `ctrl.cancel()` for lifecycle; `ctrl.state` has `{ started, paused, cancelled, loaded, total, rangeSupported }`.

## API reference

### Attachment picker

* **`registerAttachmentPicker(): void`** — Registers the `<attachment-picker>` custom element for the current window. Idempotent.

* **`renderAttachment(props, container?): HTMLElement`** — Renders the attachment picker with the given props into `container` (default `document.body`). Registers the element if needed. Returns the created element.

* **Type `AttachmentPickerProps`** — Required: `baseUrl`, `remoteURL`, `rootPath`, `token`. Optional: `mode?: 'upload' | 'picker'`, `multiSelect?: boolean`; labels `title`, `filePickerText`, `uploadFileText`, `uploadFolderText`, `modalTitle`, `pickerButtonLabel`; context `context`, `contextName`, `contextId`, `owner`; `dropdownPlacement?: 'top' | 'bottom'`. See the type definition for the full list.

### Upload

* **`getUploaderForConfig(config: NexusFilePickerConfig): Uploader`** — Returns the shared uploader instance for the given DAV config (cached per config). Use for direct control (queue, pause, progress).

* **`uploadOne(config, destinationPath, file): UploadOneResult`** — Uploads a single file to `destinationPath` (relative to config root). Returns a cancelable promise resolving to the `Upload`.

* **`uploadMultiple(config, destinationPath, files): UploadBatchResult`** — Uploads multiple files into one folder; no directory structure preserved. Returns a cancelable promise of `Upload[]`.

* **`uploadBatch(config, destinationPath, filesAndDirs, conflictCallback?): UploadBatchResult`** — Uploads files and/or directories (e.g. from `<input webkitdirectory>`), preserving structure. Optional `conflictCallback(nodes, currentPath)` for rename/skip. Returns a cancelable promise of `Upload[]`.

* **`UploadStatus`** — Enum (re-exported from nextcloud-upload). Values: `INITIALIZED`, `UPLOADING`, `ASSEMBLING`, `FINISHED`, `CANCELLED`, `FAILED`. Used on `Upload.status`.

* **Types:** `NexusFilePickerConfig` — base DAV config plus optional context. `NexusFilePickerUploadConfig` — extends it with `owner`; use when upload behavior depends on owner/context.

### Download

* **`createDownloadController(link, token, options?): { start, pause, resume, cancel, state }`** — Creates a controller for resumable/cancelable download. **Options:** `filename?: string`, `onProgress?: (p: DownloadProgress) => void`, `eventTarget?: EventTarget`. Returned object: `start()` to begin, `pause()` / `resume()` when range is supported, `cancel()` to abort; `state` holds `{ started, paused, cancelled, loaded, total, rangeSupported }`. Progress and lifecycle events can be observed via `onProgress` or `eventTarget` (e.g. `download:progress`, `download:complete`).

### HTTP / context

* **`postContext(baseUrl, token, context, contextId, fileId): Promise<PostDataModel | null>`** — POSTs file context to the Spreed/context API (e.g. for UPLOAD\_FINISHED). Returns response data or `null` if no `fileId`.

* **`getDirectDownloadLink(baseUrl, token): Promise<Object>`** — GETs the direct download link JSON from `baseUrl?token=...`.

## Configuration types

* **NexusFilePickerConfig** — `remoteURL`, `rootPath`, `token`; optional `context`, `contextName`, `contextId`. Used for both picker and upload.

* **NexusFilePickerUploadConfig** — Extends `NexusFilePickerConfig` with optional `owner`. Use when upload/context logic depends on owner.

When `context` and `contextName` are set, uploads and the picker root use `rootPath/context/contextName` as the DAV base.

## Build / development

* **Build:** `npm run build` — cleans and builds; output is in `dist/` (ESM, UMD, and TypeScript types).

* **Dev:** `npm run dev` — local development server.

## License

See project repository for license information.