Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,123 @@ const myEntry: Entry = {
addNewFileMenuEntry(myEntry)
```

#### Register a sidebar tab

It is possible to provide your own sidebar tabs for the files app.
For this you need to create a [custom web component](https://developer.mozilla.org/en-US/docs/Web/API/Web_components),
which can either be done without any framework by using vanilla JavaScript but is also [possible with Vue](https://vuejs.org/guide/extras/web-components#building-custom-elements-with-vue).

This example will make use of the Vue framework for building a sidebar tab as this is the official UI framework for Nextcloud apps.

The sidebar tab consists of two parts:
1. The web component which will be rendered within the sidebar.
2. A definition object that provides all information needed by the files app.

##### SidebarTab definition object

This object provides the requires information such as:
- The order (to ensure a consistent tabs order)
- The display name for the tab navigation
- An icon, to be used in the tab navigation
- A callback to check if the sidebar tab is enabled for the current node shown in the sidebar.
- The web component tag name

The registration must happen in an `initScript`.

```ts
import type { ISidebarTab } from '@nextcloud/files'

import { getSidebar } from '@nextcloud/files'
import { t } from '@nextcloud/l10n'

const MyTab: ISidebarTab = {
// Unique ID of the tab
id: 'my_app',

// The display name in the tab list
displayName: t('my_app', 'Sharing'),

// Pass an SVG (string) to be used as the tab button icon
iconSvgInline: '<svg>...</svg>',

// Lower values mean a more prominent position
order: 50,

// The tag name of the web component
tagName: 'my_app-files_sidebar_tab',

// Optional callback to check if the tab should be shown
enabled({ node, folder, view }) {
// you can disable this tab for some cased based on:
// - node: The node the sidebar was opened for
// - folder: The folder currently shown in the files app
// - view: The currently active files view
return true
},

// Optional, recommended to large tabs
async onInit() {
// This is called when the tab is about to be activated the first time.
// So this can be used to do some initialization or even to define the web component.
},
}

// the you need to register it in the sidebar
getSidebar()
.registerTab(MyTab)
Comment thread
susnux marked this conversation as resolved.
```

##### SidebarTab web component

The web component needs to have those properties:
- node of type `INode`
- folder of type `IFolder`
- view of type `IView`
- active of type `boolean`

When using Vue you need to first create the Vue component:

```vue
<script setup lang="ts">
import type { IFolder, INode, IView } from '@nextcloud/files'

defineProps<{
node: INode
folder: IFolder
view: IView
active: boolean
}>()
</script>

<template>
<div>
<div>Showing node: {{ node.source }}</div>
<div>... in folder: {{ folder.source }}</div>
<div>... with view: {{ view.id }}</div>
</div>
</template>
```

Which then can be wrapped in a web component and registered.

```ts
import { getSidebar } from '@nextcloud/files'
import { defineAsyncComponent, defineCustomElement } from 'vue'

getSidebar().registerTab({
// ...

tagName: `my_app-files_sidebar_tab`,

onInit() {
Comment thread
susnux marked this conversation as resolved.
const MySidebarTab = defineAsyncComponent(() => import('./views/MySidebarTab.vue'))
// make sure to disable the shadow root to allow theming with Nextcloud provided global styles.
const MySidebarTabWebComponent = defineCustomElement(MySidebarTab, { shadowRoot: false })
customElements.define('my_app-files_sidebar_tab', MySidebarTabWebComponent)
},
})
```

### WebDAV
The `getClient` exported function returns a webDAV client that's a wrapper around [webdav's webDAV client](https://www.npmjs.com/package/webdav).
All its methods are available here.
Expand Down
15 changes: 11 additions & 4 deletions __tests__/sidebar/sidebarTab.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ describe('Sidebar tabs', () => {

it('fails with invalid tagName name', () => {
expect(() => registerSidebarTab({ ...getExampleTab(), tagName: 'MyAppSidebarTab' }))
.toThrowErrorMatchingInlineSnapshot('[Error: Sidebar tabs tagName name is invalid]')
.toThrowErrorMatchingInlineSnapshot('[Error: Sidebar tab "tagName" is invalid]')
})

it('fails with missing name', () => {
Expand Down Expand Up @@ -119,11 +119,18 @@ describe('Sidebar tabs', () => {
}).toThrowErrorMatchingInlineSnapshot('[Error: Sidebar tabs need to have a numeric order set]')
})

it('fails with missing "enabled" method', () => {
it('fails with invalid "enabled" method', () => {
expect(() => {
// @ts-expect-error mocking for testing
registerSidebarTab({ ...getExampleTab(), enabled: undefined })
}).toThrowErrorMatchingInlineSnapshot('[Error: Sidebar tabs need to have an "enabled" method]')
registerSidebarTab({ ...getExampleTab(), enabled: 'true' })
}).toThrowErrorMatchingInlineSnapshot('[Error: Sidebar tab "enabled" is not a function]')
})

it('fails with invalid "onInit" method', () => {
expect(() => {
// @ts-expect-error mocking for testing
registerSidebarTab({ ...getExampleTab(), onInit: 'not a method' })
}).toThrowErrorMatchingInlineSnapshot('[Error: Sidebar tab "onInit" is not a function]')
})
})
})
Expand Down
42 changes: 32 additions & 10 deletions lib/sidebar/SidebarTab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,19 @@ export interface ISidebarContext {
* @see https://developer.mozilla.org/en-US/docs/Web/API/Web_components
* @see https://vuejs.org/guide/extras/web-components#building-custom-elements-with-vue
*/
export interface SidebarComponent extends HTMLElement, ISidebarContext {
export interface SidebarTabComponent extends ISidebarContext {
/**
* This method is called by the files app if the sidebar tab state changes.
*
* @param active - The new active state
* The active state of the sidebar tab.
* It will be set to true if this component is the currently active tab.
*/
setActive(active: boolean): Promise<void>
active: boolean
}

/**
* The instance type of a sidebar tab web component.
*/
export type SidebarTabComponentInstance = SidebarTabComponent & HTMLElement

/**
* Implementation of a custom sidebar tab within the files app.
*/
Expand Down Expand Up @@ -69,7 +73,10 @@ export interface ISidebarTab {

/**
* The tag name of the web component.
* The web component must already be registered under that tag name with `CustomElementRegistry.define()`.
*
* The web component must be defined using this name with `CustomElementRegistry.define()`,
* either on initialization or within the `onInit` callback (preferred).
* When rendering the sidebar tab, the files app will wait for the component to be defined in the registry (`customElements.whenDefined()`).
*
* To avoid name clashes the name has to start with your appid (e.g. `your_app`).
* So in addition with the web component naming rules a good name would be `your_app-files-sidebar-tab`.
Expand All @@ -79,9 +86,20 @@ export interface ISidebarTab {
/**
* Callback to check if the sidebar tab should be shown for the selected node.
*
* If not provided, the tab will always be shown.
*
* @param context - The current context of the files app
*/
enabled: (context: ISidebarContext) => boolean
enabled?: (context: ISidebarContext) => boolean

/**
* Called when the sidebar tab is active and rendered the first time in the sidebar.
* This should be used to register the web componen (`CustomElementRegistry.define()`).
*
* The sidebar itself will anyways wait for the component to be defined in the registry (`customElements.whenDefined()`).
* But also will wait for the promise returned by this method to resolve before rendering the tab.
*/
onInit?: () => Promise<void>
}

/**
Expand Down Expand Up @@ -132,7 +150,7 @@ function validateSidebarTab(tab: ISidebarTab): void {
}

if (!tab.tagName.match(/^[a-z][a-z0-9-_]+$/)) {
throw new Error('Sidebar tabs tagName name is invalid')
throw new Error('Sidebar tab "tagName" is invalid')
}

if (!tab.displayName || typeof tab.displayName !== 'string') {
Expand All @@ -147,7 +165,11 @@ function validateSidebarTab(tab: ISidebarTab): void {
throw new Error('Sidebar tabs need to have a numeric order set')
}

if (typeof tab.enabled !== 'function') {
throw new Error('Sidebar tabs need to have an "enabled" method')
if (tab.enabled && typeof tab.enabled !== 'function') {
throw new Error('Sidebar tab "enabled" is not a function')
}

if (tab.onInit && typeof tab.onInit !== 'function') {
throw new Error('Sidebar tab "onInit" is not a function')
}
}