Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
feat: provide API to register inline actions
Signed-off-by: Ferdinand Thiessen <[email protected]>
  • Loading branch information
susnux committed Sep 1, 2025
commit 8352115d6c34ed347ffc57ec37997887014e08f1
107 changes: 86 additions & 21 deletions lib/ui/sidebar-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,9 @@
*/

import type { INode } from '@nextcloud/files'
import type { IShare, ShareType } from '../index.ts'
import type { IShare } from '../index.ts'

export interface ISidebarActionContext {
/**
* The node that should be shared
*/
node: INode

/**
* The type of the share (to be created)
*/
shareType: ShareType

/**
* The share model if the share already exists
*/
share?: IShare
}
import isSvg from 'is-svg'

export interface ISidebarAction {
/**
Expand All @@ -35,7 +20,6 @@ export interface ISidebarAction {
* The custom elements identifier must be prefixed with your apps namespace like `oca_myapp-sharing_action`.
* Also the component must implement following properties:
* - `node`: The shared node as `INode` from `@nextcloud/files`.
* - `shareType`: The type of the share as number, see `ShareType` for potential values.
* - `share`: The share model if the share already exists as object of type `IShare` (potentially undefined).
* - `onSave`: A registration method to register a on-save callback which will be called when the share is saved `(callback: () => Promise<void>) => void`
*
Expand All @@ -53,10 +37,52 @@ export interface ISidebarAction {
/**
* Check if the action is enabled for a specific share type
*
* @param shareType - The share type to check
* @param context - If the share already exists the current share context is passed
* @param share - The share
* @param node - The node that share is about
*/
enabled(share: IShare, node: INode): boolean
}

export interface ISidebarInlineAction {
/**
* Unique identifier for the action
*/
id: string

/**
* Check if the action is enabled for a specific share type
*
* @param share - The share
* @param node - The node that share is about
*/
enabled(share: IShare, node: INode): boolean

/**
* The callback handler when the user selected this action.
*
* @param share - The share
* @param node - The shared node
*/
exec(share: IShare, node: INode): void | Promise<void>

/**
* The inline svg icon to use.
*/
iconSvg: string

/**
* The localized label of this action
*
* @param share - The current share (might not be created yet)
* @param node - The shared node
*/
enabled(shareType: ShareType, context: ISidebarActionContext): boolean
label(share: IShare, node: INode): string

/**
* Order of the sidebar actions.
* A higher value means the action will be displayed first.
*/
order: number
}

/**
Expand Down Expand Up @@ -85,9 +111,48 @@ export function registerSidebarAction(action: ISidebarAction): void {
window._nc_files_sharing_sidebar_actions.set(action.id, action)
}

/**
* Register a new sidebar action
*
* @param action - The action to register
*/
export function registerSidebarInlineAction(action: ISidebarInlineAction): void {
if (!action.id) {
throw new Error('Sidebar actions must have an id')
}
if (typeof action.order !== 'number') {
throw new Error('Sidebar actions must have the "order" property')
}
if (typeof action.iconSvg !== 'string' || !isSvg(action.iconSvg)) {
throw new Error('Sidebar actions must have the "iconSvg" property')
}
if (typeof action.label !== 'function') {
throw new Error('Sidebar actions must implement the "label" method')
}
if (typeof action.exec !== 'function') {
throw new Error('Sidebar actions must implement the "exec" method')
}
if (typeof action.enabled !== 'function') {
throw new Error('Sidebar actions must implement the "enabled" method')
}
window._nc_files_sharing_sidebar_inline_actions ??= new Map<string, ISidebarInlineAction>()

if (window._nc_files_sharing_sidebar_inline_actions.has(action.id)) {
throw new Error(`Sidebar action with id "${action.id}" is already registered`)
}
window._nc_files_sharing_sidebar_inline_actions.set(action.id, action)
}

/**
* Get all registered sidebar actions
*/
export function getSidebarActions(): ISidebarAction[] {
return [...(window._nc_files_sharing_sidebar_actions?.values() ?? [])]
}

/**
* Get all registered sidebar inline actions
*/
export function getSidebarInlineActions(): ISidebarInlineAction[] {
return [...(window._nc_files_sharing_sidebar_inline_actions?.values() ?? [])]
}
3 changes: 2 additions & 1 deletion lib/window.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/

import type { ISidebarAction, ISidebarSection } from './ui/index.ts'
import type { ISidebarAction, ISidebarInlineAction, ISidebarSection } from './ui/index.ts'

declare global {
interface Window {
_nc_files_sharing_sidebar_actions?: Map<string, ISidebarAction>
_nc_files_sharing_sidebar_inline_actions?: Map<string, ISidebarInlineAction>
_nc_files_sharing_sidebar_sections?: Map<string, ISidebarSection>
}
}
8 changes: 2 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
"watch": "vite --mode development build --watch"
},
"dependencies": {
"@nextcloud/initial-state": "^3.0.0"
"@nextcloud/initial-state": "^3.0.0",
"is-svg": "^6.1.0"
},
"devDependencies": {
"@nextcloud/eslint-config": "^9.0.0-rc.5",
Expand Down
78 changes: 78 additions & 0 deletions tests/ui/sidebar-inline-action.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-3.0-or-later
* @vitest-environment happy-dom
*/

import type { ISidebarInlineAction } from '../../lib/ui/index.ts'

import { beforeEach, describe, expect, test, vi } from 'vitest'
import { getSidebarInlineActions, registerSidebarInlineAction } from '../../lib/ui/index.ts'

describe('sidebar inline action', () => {
beforeEach(() => {
delete window._nc_files_sharing_sidebar_inline_actions
})

test('register action', async () => {
registerSidebarInlineAction(createAction())
expect(window._nc_files_sharing_sidebar_inline_actions).toHaveLength(1)
})

test('register multiple actions', async () => {
registerSidebarInlineAction(createAction())
registerSidebarInlineAction({ ...createAction(), id: 'test-2' })
expect(window._nc_files_sharing_sidebar_inline_actions).toHaveLength(2)
})

test('get registered actions', async () => {
registerSidebarInlineAction(createAction())
registerSidebarInlineAction({ ...createAction(), id: 'test-2' })
expect(getSidebarInlineActions()).toHaveLength(2)
expect(getSidebarInlineActions().map(({ id }) => id)).toEqual(['test', 'test-2'])
})

test('register same action twice', async () => {
registerSidebarInlineAction(createAction())
expect(() => registerSidebarInlineAction(createAction())).toThrowErrorMatchingInlineSnapshot('[Error: Sidebar action with id "test" is already registered]')
})

test('register action with missing id', async () => {
expect(() => registerSidebarInlineAction({
...createAction(),
id: '',
})).toThrowErrorMatchingInlineSnapshot('[Error: Sidebar actions must have an id]')
})

test.for`
method
--
${'enabled'}
${'label'}
${'exec'}
`('register action with missing $method', async ({ method }) => {
const action = createAction()
// @ts-expect-error mocking for tests
delete action[method]

expect(() => registerSidebarInlineAction(action)).toThrowError()
})

test('register action with invalid icon', async () => {
expect(() => registerSidebarInlineAction({
...createAction(),
iconSvg: '<div></div>',
})).toThrowErrorMatchingInlineSnapshot('[Error: Sidebar actions must have the "iconSvg" property]')
})
})

function createAction(): ISidebarInlineAction {
return {
id: 'test',
order: 0,
iconSvg: '<svg></svg>',
enabled: vi.fn(),
label: vi.fn(),
exec: vi.fn(),
}
}