# Object List (https://v1.s3.dimah.dev/docs/react/ui/components/object-list)

`ObjectList` renders a folder catalog. Pass the object returned by
[`useObjectList`](https://v1.s3.dimah.dev/docs/react/hooks/object-list) as `list`. The component
does not fetch and does not call the hook. A server payload or your own
store works when it has the same fields.

## Preview [#preview]

<DemoPreview name="object-list-demo.tsx">
  <ObjectListDemo />
</DemoPreview>

The preview groups an in-memory list with `readObjectListLevel`. Four files
load at a time. Scroll to the end of the frame and the next page loads. Swap
`query` for your API, or for `dbObjectQuery`, without changing the component.

## Install [#install]

Complete [UI Setup](https://v1.s3.dimah.dev/docs/react/ui) first — stylesheet and toaster. Then
add this component:

<Tabs items={["npm package", "shadcn registry"]}>

<Tab value="npm package">

```tsx
import { ObjectList } from "@dimah-s3/ui";
```

</Tab>

<Tab value="shadcn registry">

```npm
npx shadcn@latest add @dimah-s3/object-list
```

</Tab>

</Tabs>

---

## Usage [#usage]

```tsx
"use client";

import {
  useDelete,
  useDownload,
  useObjectList,
  useUpload,
  type ObjectListQuery,
} from "@dimah-s3/react";
import { ObjectList, ObjectListMenu, UploadButton } from "@dimah-s3/ui";

type LibraryFile = {
  id: string;
  name: string;
  key: string;
  sizeLabel: string;
  type: string;
};

export function Library({ query }: { query: ObjectListQuery<LibraryFile> }) {
  const list = useObjectList({ query });
  const upload = useUpload({
    route: "docs",
    maxFiles: 8,
    noClick: true,
    noKeyboard: true,
    getUploadOptions: () => ({
      metadata: { folderId: list.parent ?? "" },
    }),
  });
  const download = useDownload({ route: "docs" });
  const share = useDownload({ route: "docs", disposition: "inline" });
  const remove = useDelete({ route: "docs" });

  return (
    <ObjectList
      list={list}
      getKey={(file) => file.id}
      getLabel={(file) => file.name}
      getDescription={(file) => file.sizeLabel}
      getFileType={(file) => file.type}
      upload={upload}
      header={
        <UploadButton upload={upload} size="sm" status={false} toast={false} />
      }
      delete={remove}
      getObjectKey={(file) => file.key}
      onOpenItem={(file) => {
        void share.presign(file.key, file.name).then((signed) => {
          if (signed) window.open(signed.url, "_blank", "noopener,noreferrer");
        });
      }}
      renderActions={(file) => (
        <ObjectListMenu
          objectKey={file.key}
          fileName={file.name}
          download={download}
          share={share}
          delete={remove}
          onDeleted={() => void list.refresh()}
        />
      )}
    />
  );
}
```

`getKey` and `getLabel` are the only required accessors. Pass `upload` to drop or paste files onto the open folder. Pass `delete`, `selectable`, `selectionActions`, or `onSelectionChange` to select the loaded file rows. The checkbox sits inside the row. Shift-click a checkbox to extend from the previous row. Escape clears the selection. With focus inside the list, Ctrl+A or Cmd+A selects the same rows as the header checkbox. A selected row sets `data-selected`. Files set `data-kind="file"` and folders set `data-kind="folder"`. `getObjectKey` is the stored object key when it differs from `getKey`. Pass `resolveFolderKeys` to give a folder a checkbox; bulk delete includes the keys it returns. Without it, folders stay navigation-only. `onOpenItem` runs when a file row is clicked. `renderMeta` sits beside the file name. `renderActions` is where the row menu goes. `renderItem` replaces the whole row, including the checkbox and `renderMeta`.

The header checkbox selects rows loaded so far. When `list.hasMore` is set, its name is "Select this page" and the bar says the count is on this page. Rows on the next page stay unchecked. `onSelectionChange` receives the checked files and folders from the loaded page. It also runs when the open folder changes and the selection clears. `selectionActions` renders on the selection bar beside Delete. Those controls are inert while a bulk delete is running.

`ObjectListMenu` downloads, deletes, and copies a link. Pass `onDeleted` to reload after that row is deleted. Leave `useDelete`'s `onSuccess` for your own work: a bulk delete calls `onSuccess` once per key, and the selection bar already reloads once. Copy link signs through `share` when you pass it, otherwise through `download`, then writes the URL to the clipboard. Pass `share` as a second `useDownload` with `disposition: "inline"` when the link should open in a browser while Download stays an attachment. Pass `expiresIn` to ask for a shorter TTL than the route cap. The server signs the shorter of that value and `download.expiresIn`, and the toast shows the TTL that was actually signed. Signing a link does not start a download. Pass `actions` for extra rows, such as rename. They render after Copy link and before Delete.

## Frame [#frame]

Pass `height` to keep the header in place and scroll the rows inside a fixed frame. A number is pixels. A string is a CSS length, such as `"18rem"` or `"100%"`. Pass `maxHeight` when the frame should grow with the folder until that cap, then scroll the same way. Upload progress stays with the header.

When `list.hasMore` is set, the next page loads as the end of the rows comes into view. A frame that is not full keeps loading until it is, or until there is no next page. Scrolling to the end loads the page after that.

## Filter [#filter]

Keep the search box outside `ObjectList`. Put the text in `query`, and in `queryKey` so the first page loads again. When the filter matches nothing, pass `emptyTitle` and `emptyDescription`. Otherwise the empty state says the folder is empty. Pass `empty` to replace that state.

```tsx
const [search, setSearch] = useState("");
const list = useObjectList({
  query: ({ parent, cursor, signal }) =>
    loadFiles({ folder: parent, search, cursor, signal }),
  queryKey: [search],
});
```

## Upload into the open folder [#upload-into-the-open-folder]

Pass the same `useUpload` to `upload` and, for the file picker, to `UploadButton` in `header`. Set `noClick` and `noKeyboard` on the hook. The list surface accepts a drop and a paste, and it does not open the picker when a row is clicked. The list renders the hook's file input itself, so `upload.open()` from any trigger — a menu item, a split button, or an empty-state call to action — opens the picker.

`getUploadOptions` reads `list.parent` and sends it as `metadata.folderId`. It runs as each file starts, so it sees the folder that is open at that moment. Read `clientMetadata.folderId` in `upload.object` or `upload.guard` and store it on your file row. The library does not write that id into the object key, and it does not retarget a drop onto the folder under the pointer.

When the batch settles and at least one file succeeded, `ObjectList` calls `list.refresh()` once. A batch where some files fail still reloads, then the status strip stays on the failure until `upload.reset()`. Leave `onSuccess` for your own work. A second `list.refresh()` there loads the page twice. Set `toast={false}` and `status={false}` on the header button so the list owns progress and toasts.

```tsx
const upload = useUpload({
  route: "docs",
  maxFiles: 8,
  noClick: true,
  noKeyboard: true,
  getUploadOptions: () => ({
    metadata: { folderId: list.parent ?? "" },
  }),
});

<ObjectList
  list={list}
  getKey={(file) => file.id}
  getLabel={(file) => file.name}
  upload={upload}
  header={
    <UploadButton upload={upload} size="sm" status={false} toast={false} />
  }
/>;
```

`trail` drives the breadcrumb. `useObjectList` fills it when you open folders with `openFolder` and `navigate`. For a controlled URL, keep the same path in `defaultTrail` on the first render, or pass `trail` on the `list` object. A path longer than the root plus two folders collapses the earlier folders into a menu.

## Storage [#storage]

Return `usage` from the query when you know how full the catalog is. `useObjectList` keeps it on `list.usage`. A later page can omit it. A refresh or a new folder that omits it clears the line.

`dbObjectQuery` forwards the `usage` already on `listObjects`: bytes and file count for the whole scope, not the open folder. Pass your own caps as `usageLimits`. Those are the same numbers as `db({ quota })` when that plugin enforces the limit. The component does not read the server config. Without `usageLimits`, the line is the totals only. Without `usage`, nothing is shown.

```tsx
<ObjectList
  list={list}
  getKey={(file) => file.id}
  getLabel={(file) => file.name}
  usageLimits={{ maxBytes: 100 * 1024 * 1024, maxFiles: 50 }}
/>
```

A query you write yourself uses the same shape:

```tsx
return {
  items: page.files,
  nextCursor: page.nextCursor,
  usage: { totalBytes: page.totalBytes, objectCount: page.fileCount },
};
```

The meter follows whichever cap is closer to full. At the cap, the line and the meter use the destructive color.

## Folders [#folders]

`ObjectList` does not invent folders. The first page of `query` returns them. [`dbObjectQuery`](https://v1.s3.dimah.dev/docs/react/hooks/object-list#with-db) lists objects and sends `parent` as a key prefix. Generated keys contain a uuid segment, so that prefix is not a folder tree. Use it for a flat catalog.

For a tree, keep folders in your own table and return them from `query`. Return `folders` only on the first page. Later pages append `items`.

```tsx
const list = useObjectList({
  query: async ({ parent, cursor, signal }) => {
    const page = await loadLibrary({ folderId: parent, cursor, signal });
    return {
      folders: cursor ? undefined : page.folders,
      items: page.files,
      nextCursor: page.nextCursor,
    };
  },
});
```

`readObjectListLevel` groups a complete in-memory list. Pass the full set for that listing. One page of a database query cannot be grouped after the fact. Upload into the open folder from the section above.

## Menu [#menu]

```ts
import type { ObjectListMenuProps } from "@dimah-s3/ui";
```

<AutoTypeTable
  path="packages/ui/src/components/dimah-s3/list/object-list-menu.tsx"
  name="ObjectListMenuProps"
/>

```ts
import type { ObjectListMenuAction } from "@dimah-s3/ui";
```

<AutoTypeTable
  path="packages/ui/src/components/dimah-s3/list/object-list-menu.tsx"
  name="ObjectListMenuAction"
/>

## Props [#props]

```ts
import type { ObjectListProps } from "@dimah-s3/ui";
```

<AutoTypeTable
  path="packages/ui/src/components/dimah-s3/list/object-list.tsx"
  name="ObjectListProps"
/>
