Skip to main content

CnFilesBrowser

A folder of Nextcloud files, on any page, built from the Files app's own primitives rather than a copy of its screen.

What it borrows, and from where​

  • Rows are @nextcloud/files nodes read over WebDAV with the same PROPFIND the Files app sends, so a node here carries what a node there carries: id, mime, size, mtime, permissions, attributes.
  • Each row's menu is getFileActions(): the actions the Files app and its plugins registered on the page (download, delete, rename, favourite, move and copy, sharing status, tags, lock, open in Files). They run with the context the Files app hands them, so a plugin's action does what it does in the Files app.
  • The New menu is getNewFileMenuEntries(folder): new folder, a template, a file request, whatever the plugins offer for this folder, beside a plain upload that PUTs over DAV.
  • Crumbs are NcBreadcrumbs, drawing the whole trail from the user's files root the way the Files app does: the folders above the browser's root link into the Files app, the root and everything beneath it navigate in place. Icons are the theme's own mime icons through OC.MimeType; image previews come from the core preview endpoint.

What the host page has to do​

The Files app registers its actions and menu entries only on pages that ask for them. The host dispatches Nextcloud's OCA\Files\Event\LoadAdditionalScriptsEvent server-side (the plugins' scripts) and adds the Files app's init script (Util::addScript('files', 'init'), the core actions). Dispatching OCA\Viewer\Event\LoadViewer as well lets the view action open a file over the page.

On a page that did nothing of this, the row menu holds only Show in Files, which is the honest fallback.

What is not borrowed​

The Files app's list itself (column filters, selection bar, inline rename) is a Vue app bound to the Files router and stores, and so is its details sidebar on Nextcloud 34. Neither can be mounted elsewhere. The actions that need them are left out by name (ACTIONS_NEEDING_THE_FILES_PAGE) and Show in Files opens the Files app on the file for the rest.

The registry is versioned​

@nextcloud/files keeps its registries under window._nc_files_scope.v4_0. The page's plugins register into the copy the server ships; this component reads through the library's bundled copy. Both have to be the same major, which is why this library depends on @nextcloud/files 4.x.

Usage​

<CnFilesBrowser
root-path="/Open Registers/Cases/6c0d…"
@changed="onFilesChanged" />

Resolve the root for an OpenRegister object with resolveObjectFolder():

import { resolveObjectFolder } from '@conduction/nextcloud-vue'
import { getCurrentUser } from '@nextcloud/auth'
import { generateRemoteUrl } from '@nextcloud/router'

const rootPath = await resolveObjectFolder({
apiBase: '/apps/openregister/api',
register: 'dossiq',
schema: 'case',
objectId,
uid: getCurrentUser().uid,
remoteUrl: generateRemoteUrl('dav'),
})

It reads the object's @self.folder file id and turns it into a path with a DAV search; null means the object has no folder or the user cannot see it, and the caller falls back to the object's files endpoint. CnFilesTab does exactly this.

The host's actions and linked rows​

A host that keeps a record about each file (dossiq's ZGW document record, say) can put its own action on every file row and show files that belong to the object without living in its folder:

<CnFilesBrowser
root-path="/Open Registers/Cases/6c0d…"
:row-actions="[{ id: 'document-properties', label: 'Document properties', icon: 'FileDocumentEditOutline', type: 'open-modal', target: 'DocumentMetadataDialog' }]"
:linked-items="linkedDocuments" />

A row action is dispatched the way a widget's row action is: through the page's cnDispatchAction when the browser sits in a CnPageRenderer tree, else through the bare dispatcher. Folders get no host action. A linked row is read-only here; what changes it lives where the file does.

Columns the host declares​

Set columns and the table shows what this page needs rather than what a file manager needs. A document list on a case wants sender, recipient, direction and a scan verdict; none of those live on the node, and all of them can be a column.

"columns": [
"name",
{ "key": "sender", "label": "Sender", "source": "row" },
{ "key": "scan", "label": "Scan", "source": "attribute",
"attribute": "{http://owncloud.org/ns}av-status", "formatter": "scanVerdict" },
"modified"
]

Three sources, three places a value comes from:

  • node reads a property of the file itself, the one the DAV listing already carried.
  • attribute reads a DAV property, and the browser adds it to the PROPFIND before listing, so the value is there with the rows rather than one request per file later.
  • row reads rowData[fileid][key]. Pass rowData as a function and it is called once with the whole listing, so the host fetches its projection in one request. Without rowData the cell is empty and nothing is thrown.

Every declared cell renders through CnCellRenderer, so a formatter means here exactly what it means in a table.

Who decides what​

  • The host decides membership. columns is the list of columns this browser has, in the host's order. Declare none and you get name, size and modified, as before.
  • The user decides visibility, and only downward. Give preferenceApp and the toolbar offers a Columns chooser that hides any declared column and remembers it per user. It can never add one: the chooser is built by walking the host's declaration, so a column the host removed is not listed, cannot be ticked back on, and does not render however an old stored choice reads.

A node column sorts through the Files app's own sorter. A row or attribute column with sortable: true sorts on its resolved value within the listed folder, folders still first, and a file with no value sorts last in both directions rather than pushing the filled rows out of sight.

Start with one declared column and rowData as a function, then add the chooser once the set is settled.

Props​

PropTypeDefaultDescription
rootPathStringrequiredThe folder the browser is rooted at, relative to the current user's files root. The browser never navigates above it.
rootLabelStringnullWhat the root crumb reads. Null shows the folder's own name, as the Files app does; pass a label when the folder on disk is a uuid and the host knows a better name.
newLabelString'New'Label of the New menu.
uploadLabelString'Upload files'Label of the upload entry in the New menu.
newFolderLabelString'New folder'Label of the new-folder entry and its dialog.
renameLabelString'Rename'Label of the rename action and its dialog. The Files app's own rename is its list's inline input, so the browser renames through a dialog and a DAV move.
showInFilesLabelString'Show in Files'Label of the link that opens the file in the Files app.
emptyLabelString'This folder is empty'Title of the empty state.
emptyHintString'Drop files here, or use New'Line under the empty state's title.
retryLabelString'Try again'Label of the retry button on a failed listing.
rowActionsArray[]The host's own actions on each file row (never on a folder), declared like any manifest action: { id, label, icon?, type, target?, props?, handler?, args? }. Dispatched through the page's action runner (cnDispatchAction, provided by CnPageRenderer) with the file merged in: an open-modal action's props gain fileId, fileName and path; a handler action's args gain the node. icon is an MDI icon name.
newActionsArray[]The host's own entries in the New menu, after the ones the Files app and its plugins register. Declared like a row action ({ id, label, icon?, type, target?, props?, handler?, args? }) and dispatched the same way, but with the folder rather than a row: an open-modal action's props gain path, a handler action's args gain the folder node. Use it for "new from template" or "request a file from a party".
linkedItemsArray[]Rows that are not nodes of this folder: files the host joined from another object's folder, shown after the folder's own rows with open and download only. Each is { id, name, mime?, size?, mtime?, href?, downloadHref?, note?, noteHref? }; note says where the file lives, noteHref links there.
columnsArray[]The columns the browser shows, in order. A string names a built-in (name, size, modified, owner, type, tags); an object declares its own: { key, label, source, attribute, formatter, sortable }, where source is node, attribute or row. Declaring none keeps today's name, size and modified.
rowDataObject | FunctionnullThe host's per-file data for source: 'row' columns, keyed by file id. A function is called once per folder with the listed nodes, so a projection of ninety files is one request.
preferenceAppString''The app id the Columns chooser stores this user's choice under. Unset means no chooser, and every declared column renders.
preferenceKeyString'files-browser-columns'The key the choice is stored under, so two browsers in one app remember separately.
columnsLabelString'Columns'Label of the Columns chooser.
openLinkedLabelString'Open'Label of a linked row's open action.
downloadLabelString'Download'Label of a linked row's download action.

Events​

EventPayloadDescription
changednoneThe folder's contents changed through this browser: an upload, a new folder, or an action that ran.

Helpers​

Exported alongside the component:

  • resolveObjectFolder({ apiBase, register, schema, objectId, uid, remoteUrl }) — the object's folder as a user-relative path, or null.
  • userRelativePathFromHref(href, uid) — a DAV href as a path under the user's files root.
  • crumbsFor(rootPath, currentPath, rootLabel?) — the whole trail, outermost first; crumbs above the root carry aboveRoot: true.
  • joinPath(dir, name) — one slash between.
  • ACTIONS_NEEDING_THE_FILES_PAGE — the registered action ids that cannot run away from the Files page.