Skip to content

Commit 0a45596

Browse files
committed
feat(sidebar): properly implement sidebar Proxy for Nextcloud 33+
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
1 parent b3b2f4d commit 0a45596

6 files changed

Lines changed: 203 additions & 34 deletions

File tree

lib/fileListFilters.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export interface IFileListFilterChip {
3131
/**
3232
* Handler to be called on click
3333
*/
34-
onclick: () => void
34+
onclick(): void
3535
}
3636

3737
/**

lib/sidebar/Sidebar.ts

Lines changed: 86 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
55

6+
import type { INode } from '../node/node.ts'
7+
import type { ISidebarAction } from './SidebarAction.ts'
68
import type { ISidebarContext, ISidebarTab } from './SidebarTab.ts'
79

8-
import { getSidebarTabs, registerSidebarTab } from './SidebarTab.ts'
10+
import { registerSidebarAction } from './SidebarAction.ts'
11+
import { registerSidebarTab } from './SidebarTab.ts'
912

1013
export interface ISidebar {
1114
/**
@@ -17,17 +20,47 @@ export interface ISidebar {
1720
/**
1821
* The current open state of the sidebar
1922
*/
20-
readonly open: boolean
23+
readonly isOpen: boolean
2124

2225
/**
23-
* Open or close the sidebar
26+
* The currently active sidebar tab id
27+
*/
28+
readonly activeTab?: string
29+
30+
/**
31+
* The currently opened node in the sidebar
32+
*/
33+
readonly node?: INode
34+
35+
/**
36+
* Open the sidebar for a specific node.
37+
*
38+
* When the sidebar is fully opened the `files:sidebar:opened` event is emitted,
39+
* see also `@nextcloud/event-bus`.
40+
*
41+
* @param node - The node to open the sidebar for
42+
* @param tab - The tab to open by default
43+
*/
44+
open(node: INode, tab?: string): void
45+
46+
/**
47+
* Close the sidebar.
2448
*
25-
* @param open - The new open state
49+
* When the sidebar is fully closed the `files:sidebar:closed` event is emitted,
50+
* see also `@nextcloud/event-bus`.
2651
*/
27-
setOpen(open: boolean): void
52+
close(): void
2853

2954
/**
30-
* Register a new sidebar tab
55+
* Set the active sidebar tab
56+
*
57+
* @param tabId - The tab to set active
58+
*/
59+
setActiveTab(tabId: string): void
60+
61+
/**
62+
* Register a new sidebar tab.
63+
* This should ideally be done on app initialization using Nextcloud init scripts.
3164
*
3265
* @param tab - The sidebar tab to register
3366
*/
@@ -38,6 +71,22 @@ export interface ISidebar {
3871
* If a node is passed only the enabled tabs are retrieved.
3972
*/
4073
getTabs(context?: ISidebarContext): ISidebarTab[]
74+
75+
/**
76+
* Get all registered sidebar actions.
77+
*
78+
* If a context is provided only the enabled actions are returned.
79+
*
80+
* @param context - The context
81+
*/
82+
getActions(context?: ISidebarContext): ISidebarAction[]
83+
84+
/**
85+
* Register a new sidebar action.
86+
*
87+
* @param action - The action to register
88+
*/
89+
registerAction(action: ISidebarAction): void
4190
}
4291

4392
/**
@@ -50,36 +99,52 @@ export interface ISidebar {
5099
*/
51100
class SidebarProxy implements ISidebar {
52101

102+
get #impl(): Omit<ISidebar, 'available' | 'registerTab' | 'registerAction'> | undefined {
103+
return window.OCA?.Files?._sidebar?.()
104+
}
105+
53106
get available(): boolean {
54-
return !!window.OCA?.Files?.Sidebar
107+
return !!this.#impl
108+
}
109+
110+
get isOpen(): boolean {
111+
return this.#impl?.isOpen ?? false
112+
}
113+
114+
get activeTab(): string | undefined {
115+
return this.#impl?.activeTab
116+
}
117+
118+
get node(): INode | undefined {
119+
return this.#impl?.node
55120
}
56121

57-
get open(): boolean {
58-
return !!window.OCA?.Files?.Sidebar?.state.file
122+
open(node: INode, tab?: string): void {
123+
this.#impl?.open(node, tab)
59124
}
60125

61-
setOpen(open: boolean): void {
62-
if (open) {
63-
window.OCA?.Files?.Sidebar?.open()
64-
} else {
65-
window.OCA?.Files?.Sidebar?.close()
66-
}
126+
close(): void {
127+
this.#impl?.close()
67128
}
68129

69130
setActiveTab(tabId: string): void {
70-
window.OCA?.Files?.Sidebar?.setActiveTab(tabId)
131+
this.#impl?.setActiveTab(tabId)
71132
}
72133

73134
registerTab(tab: ISidebarTab): void {
74135
registerSidebarTab(tab)
75136
}
76137

77138
getTabs(context?: ISidebarContext): ISidebarTab[] {
78-
const tabs = getSidebarTabs()
79-
if (context) {
80-
return tabs.filter((tab) => tab.enabled(context))
81-
}
82-
return tabs
139+
return this.#impl?.getTabs(context) ?? []
140+
}
141+
142+
getActions(context?: ISidebarContext): ISidebarAction[] {
143+
return this.#impl?.getActions(context) ?? []
144+
}
145+
146+
registerAction(action: ISidebarAction): void {
147+
registerSidebarAction(action)
83148
}
84149

85150
}

lib/sidebar/SidebarAction.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { ISidebarContext } from './SidebarTab.ts'
7+
8+
import logger from '../utils/logger.ts'
9+
10+
/**
11+
* Implementation of a custom sidebar tab within the files app.
12+
*/
13+
export interface ISidebarAction {
14+
/**
15+
* Unique id of the sidebar tab.
16+
* This has to conform to the HTML id attribute specification.
17+
*/
18+
id: string
19+
20+
/**
21+
* The order of this tab.
22+
* Use a low number to make this tab ordered in front.
23+
*/
24+
order: number
25+
26+
/**
27+
* The localized name of the sidebar tab.
28+
*
29+
* @param context - The current context of the files app
30+
*/
31+
displayName(context: ISidebarContext): string
32+
33+
/**
34+
* The icon, as SVG, of the sidebar tab.
35+
*
36+
* @param context - The current context of the files app
37+
*/
38+
iconSvgInline(context: ISidebarContext): string
39+
40+
/**
41+
* Callback to check if the sidebar tab should be shown for the selected node.
42+
*
43+
* @param context - The current context of the files app
44+
*/
45+
enabled(context: ISidebarContext): boolean
46+
47+
/**
48+
* Handle the sidebar action.
49+
*
50+
* @param context - The current context of the files app
51+
*/
52+
onClick(context: ISidebarContext): void
53+
}
54+
55+
/**
56+
* Register a new sidebar action.
57+
*
58+
* @param action - The sidebar action to register
59+
* @throws If the provided action is not a valid sidebar action and thus cannot be registered.
60+
*/
61+
export function registerSidebarAction(action: ISidebarAction): void {
62+
validateSidebarAction(action)
63+
64+
window._nc_files_sidebar_actions ??= new Map<string, ISidebarAction>()
65+
if (window._nc_files_sidebar_actions.has(action.id)) {
66+
logger.warn(`Sidebar action with id "${action.id}" already registered. Skipping.`)
67+
return
68+
}
69+
window._nc_files_sidebar_actions.set(action.id, action)
70+
logger.debug(`New sidebar action with id "${action.id}" registered.`)
71+
}
72+
73+
/**
74+
* Get all currently registered sidebar actions.
75+
*/
76+
export function getSidebarActions(): ISidebarAction[] {
77+
if (window._nc_files_sidebar_actions) {
78+
return [...window._nc_files_sidebar_actions.values()]
79+
}
80+
return []
81+
}
82+
83+
/**
84+
* Check if a given sidebar action object implements all necessary fields.
85+
*
86+
* @param action - The sidebar action to validate
87+
*/
88+
function validateSidebarAction(action: ISidebarAction): void {
89+
if (typeof action !== 'object') {
90+
throw new Error('Sidebar action is not an object')
91+
}
92+
93+
if (!action.id || (typeof action.id !== 'string') || action.id !== CSS.escape(action.id)) {
94+
throw new Error('Sidebar actions need to have an id conforming to the HTML id attribute specifications')
95+
}
96+
97+
if (!action.displayName || typeof action.displayName !== 'function') {
98+
throw new Error('Sidebar actions need to have a displayName function')
99+
}
100+
101+
if (!action.iconSvgInline || typeof action.iconSvgInline !== 'function') {
102+
throw new Error('Sidebar actions need to have a iconSvgInline function')
103+
}
104+
105+
if (!action.enabled || typeof action.enabled !== 'function') {
106+
throw new Error('Sidebar actions need to have an enabled function')
107+
}
108+
109+
if (!action.onClick || typeof action.onClick !== 'function') {
110+
throw new Error('Sidebar actions need to have an onClick function')
111+
}
112+
}

lib/sidebar/SidebarTab.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -149,15 +149,4 @@ function validateSidebarTab(tab: ISidebarTab): void {
149149
if (typeof tab.enabled !== 'function') {
150150
throw new Error('Sidebar tabs need to have an "enabled" method')
151151
}
152-
153-
// now check the custom element constructor
154-
const tagConstructor = window.customElements.get(tab.tagName)
155-
if (!tagConstructor) {
156-
throw new Error('Sidebar tab element not registered')
157-
}
158-
159-
if (!('setActive' in tagConstructor.prototype)) {
160-
// we cannot check properties like `node` or `view` because those are not necessarily defined in the prototype.
161-
throw new Error('Sidebar tab elements must have the `setActive` method')
162-
}
163152
}

lib/sidebar/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
55

6-
export type { ISidebarContext, ISidebarTab, SidebarComponent } from './SidebarTab.ts'
76
export * from './Sidebar.ts'
7+
export * from './SidebarAction.ts'
8+
export * from './SidebarTab.ts'

lib/window.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515

1616
import type { DavProperty } from './dav/index.ts'
1717
import type { ISidebarTab } from './sidebar/index.ts'
18+
import type { ISidebarAction } from './sidebar/SidebarAction.ts'
1819

1920
export {}
2021

@@ -31,6 +32,7 @@ declare global {
3132
_nc_newfilemenu?: NewMenu
3233
_nc_navigation?: Navigation
3334
_nc_filelist_filters?: Map<string, IFileListFilter>
35+
_nc_files_sidebar_actions?: Map<string, ISidebarAction>
3436
_nc_files_sidebar_tabs?: Map<string, ISidebarTab>
3537

3638
_oc_config?: {

0 commit comments

Comments
 (0)