Skip to content

Commit 7c2ff4a

Browse files
authored
Merge pull request #1459 from nextcloud-libraries/feat/postpone-register
feat(sidebar): allow to postpone definition of sidebar tabs
2 parents ff2cffd + 6bf0138 commit 7c2ff4a

3 files changed

Lines changed: 160 additions & 14 deletions

File tree

README.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,123 @@ const myEntry: Entry = {
5353
addNewFileMenuEntry(myEntry)
5454
```
5555

56+
#### Register a sidebar tab
57+
58+
It is possible to provide your own sidebar tabs for the files app.
59+
For this you need to create a [custom web component](https://developer.mozilla.org/en-US/docs/Web/API/Web_components),
60+
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).
61+
62+
This example will make use of the Vue framework for building a sidebar tab as this is the official UI framework for Nextcloud apps.
63+
64+
The sidebar tab consists of two parts:
65+
1. The web component which will be rendered within the sidebar.
66+
2. A definition object that provides all information needed by the files app.
67+
68+
##### SidebarTab definition object
69+
70+
This object provides the requires information such as:
71+
- The order (to ensure a consistent tabs order)
72+
- The display name for the tab navigation
73+
- An icon, to be used in the tab navigation
74+
- A callback to check if the sidebar tab is enabled for the current node shown in the sidebar.
75+
- The web component tag name
76+
77+
The registration must happen in an `initScript`.
78+
79+
```ts
80+
import type { ISidebarTab } from '@nextcloud/files'
81+
82+
import { getSidebar } from '@nextcloud/files'
83+
import { t } from '@nextcloud/l10n'
84+
85+
const MyTab: ISidebarTab = {
86+
// Unique ID of the tab
87+
id: 'my_app',
88+
89+
// The display name in the tab list
90+
displayName: t('my_app', 'Sharing'),
91+
92+
// Pass an SVG (string) to be used as the tab button icon
93+
iconSvgInline: '<svg>...</svg>',
94+
95+
// Lower values mean a more prominent position
96+
order: 50,
97+
98+
// The tag name of the web component
99+
tagName: 'my_app-files_sidebar_tab',
100+
101+
// Optional callback to check if the tab should be shown
102+
enabled({ node, folder, view }) {
103+
// you can disable this tab for some cased based on:
104+
// - node: The node the sidebar was opened for
105+
// - folder: The folder currently shown in the files app
106+
// - view: The currently active files view
107+
return true
108+
},
109+
110+
// Optional, recommended to large tabs
111+
async onInit() {
112+
// This is called when the tab is about to be activated the first time.
113+
// So this can be used to do some initialization or even to define the web component.
114+
},
115+
}
116+
117+
// the you need to register it in the sidebar
118+
getSidebar()
119+
.registerTab(MyTab)
120+
```
121+
122+
##### SidebarTab web component
123+
124+
The web component needs to have those properties:
125+
- node of type `INode`
126+
- folder of type `IFolder`
127+
- view of type `IView`
128+
- active of type `boolean`
129+
130+
When using Vue you need to first create the Vue component:
131+
132+
```vue
133+
<script setup lang="ts">
134+
import type { IFolder, INode, IView } from '@nextcloud/files'
135+
136+
defineProps<{
137+
node: INode
138+
folder: IFolder
139+
view: IView
140+
active: boolean
141+
}>()
142+
</script>
143+
144+
<template>
145+
<div>
146+
<div>Showing node: {{ node.source }}</div>
147+
<div>... in folder: {{ folder.source }}</div>
148+
<div>... with view: {{ view.id }}</div>
149+
</div>
150+
</template>
151+
```
152+
153+
Which then can be wrapped in a web component and registered.
154+
155+
```ts
156+
import { getSidebar } from '@nextcloud/files'
157+
import { defineAsyncComponent, defineCustomElement } from 'vue'
158+
159+
getSidebar().registerTab({
160+
// ...
161+
162+
tagName: `my_app-files_sidebar_tab`,
163+
164+
onInit() {
165+
const MySidebarTab = defineAsyncComponent(() => import('./views/MySidebarTab.vue'))
166+
// make sure to disable the shadow root to allow theming with Nextcloud provided global styles.
167+
const MySidebarTabWebComponent = defineCustomElement(MySidebarTab, { shadowRoot: false })
168+
customElements.define('my_app-files_sidebar_tab', MySidebarTabWebComponent)
169+
},
170+
})
171+
```
172+
56173
### WebDAV
57174
The `getClient` exported function returns a webDAV client that's a wrapper around [webdav's webDAV client](https://www.npmjs.com/package/webdav).
58175
All its methods are available here.

__tests__/sidebar/sidebarTab.spec.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ describe('Sidebar tabs', () => {
7777

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

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

122-
it('fails with missing "enabled" method', () => {
122+
it('fails with invalid "enabled" method', () => {
123123
expect(() => {
124124
// @ts-expect-error mocking for testing
125-
registerSidebarTab({ ...getExampleTab(), enabled: undefined })
126-
}).toThrowErrorMatchingInlineSnapshot('[Error: Sidebar tabs need to have an "enabled" method]')
125+
registerSidebarTab({ ...getExampleTab(), enabled: 'true' })
126+
}).toThrowErrorMatchingInlineSnapshot('[Error: Sidebar tab "enabled" is not a function]')
127+
})
128+
129+
it('fails with invalid "onInit" method', () => {
130+
expect(() => {
131+
// @ts-expect-error mocking for testing
132+
registerSidebarTab({ ...getExampleTab(), onInit: 'not a method' })
133+
}).toThrowErrorMatchingInlineSnapshot('[Error: Sidebar tab "onInit" is not a function]')
127134
})
128135
})
129136
})

lib/sidebar/SidebarTab.ts

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,19 @@ export interface ISidebarContext {
3232
* @see https://developer.mozilla.org/en-US/docs/Web/API/Web_components
3333
* @see https://vuejs.org/guide/extras/web-components#building-custom-elements-with-vue
3434
*/
35-
export interface SidebarComponent extends HTMLElement, ISidebarContext {
35+
export interface SidebarTabComponent extends ISidebarContext {
3636
/**
37-
* This method is called by the files app if the sidebar tab state changes.
38-
*
39-
* @param active - The new active state
37+
* The active state of the sidebar tab.
38+
* It will be set to true if this component is the currently active tab.
4039
*/
41-
setActive(active: boolean): Promise<void>
40+
active: boolean
4241
}
4342

43+
/**
44+
* The instance type of a sidebar tab web component.
45+
*/
46+
export type SidebarTabComponentInstance = SidebarTabComponent & HTMLElement
47+
4448
/**
4549
* Implementation of a custom sidebar tab within the files app.
4650
*/
@@ -69,7 +73,10 @@ export interface ISidebarTab {
6973

7074
/**
7175
* The tag name of the web component.
72-
* The web component must already be registered under that tag name with `CustomElementRegistry.define()`.
76+
*
77+
* The web component must be defined using this name with `CustomElementRegistry.define()`,
78+
* either on initialization or within the `onInit` callback (preferred).
79+
* When rendering the sidebar tab, the files app will wait for the component to be defined in the registry (`customElements.whenDefined()`).
7380
*
7481
* To avoid name clashes the name has to start with your appid (e.g. `your_app`).
7582
* So in addition with the web component naming rules a good name would be `your_app-files-sidebar-tab`.
@@ -79,9 +86,20 @@ export interface ISidebarTab {
7986
/**
8087
* Callback to check if the sidebar tab should be shown for the selected node.
8188
*
89+
* If not provided, the tab will always be shown.
90+
*
8291
* @param context - The current context of the files app
8392
*/
84-
enabled: (context: ISidebarContext) => boolean
93+
enabled?: (context: ISidebarContext) => boolean
94+
95+
/**
96+
* Called when the sidebar tab is active and rendered the first time in the sidebar.
97+
* This should be used to register the web componen (`CustomElementRegistry.define()`).
98+
*
99+
* The sidebar itself will anyways wait for the component to be defined in the registry (`customElements.whenDefined()`).
100+
* But also will wait for the promise returned by this method to resolve before rendering the tab.
101+
*/
102+
onInit?: () => Promise<void>
85103
}
86104

87105
/**
@@ -132,7 +150,7 @@ function validateSidebarTab(tab: ISidebarTab): void {
132150
}
133151

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

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

150-
if (typeof tab.enabled !== 'function') {
151-
throw new Error('Sidebar tabs need to have an "enabled" method')
168+
if (tab.enabled && typeof tab.enabled !== 'function') {
169+
throw new Error('Sidebar tab "enabled" is not a function')
170+
}
171+
172+
if (tab.onInit && typeof tab.onInit !== 'function') {
173+
throw new Error('Sidebar tab "onInit" is not a function')
152174
}
153175
}

0 commit comments

Comments
 (0)